Ngày nay, chúng ta có nhiều nguyên tắc, phương pháp thực hành tốt (SOLID, DRY, KISS), mô hình GoF và hơn thế nữa. Tất cả họ đều đang cố gắng giúp chúng tôi, những nhà phát triển, viết mã tốt, sạch, dễ bảo trì và dễ hiểu.
GRASP là từ viết tắt của Các mẫu phần mềm phân công trách nhiệm chung.
Đó là một tập hợp các đề xuất, nguyên tắc và mẫu thực sự tốt và có thể làm cho mã của chúng tôi tốt hơn nhiều. Hãy cùng xem danh sách này:
- Chuyên gia thông tin
- Người sáng tạo
- Bộ điều khiển
- Khớp nối thấp
- Độ gắn kết cao
- Chế tạo thuần túy
- Chuyển hướng
- Các biến thể được bảo vệ
- Đa hình
Hôm nay, chúng ta sẽ tìm hiểu 2 nguyên tắc đầu tiên: Chuyên gia thông tin và Người sáng tạo.
Chuyên gia thông tin
Chuyên gia thông tin có thể là người quan trọng nhất trong tất cả các mẫu GRASP. Mẫu này nói rằng tất cả các phương thức làm việc với dữ liệu (biến, trường) phải ở cùng một nơi mà dữ liệu (biến hoặc trường) tồn tại.
Tôi biết, tôi biết, nó có vẻ không rõ ràng lắm, vì vậy hãy xem một ví dụ. Chúng tôi muốn tạo ra chức năng sẽ tính toán tổng các mặt hàng đã đặt hàng của chúng tôi.
Hãy tưởng tượng rằng chúng ta có 4 tệp: main.js, OrderList, OrderItem và Product.
Sản phẩm có thể chứa id, tên và giá (và nhiều trường khác, không liên quan đến ví dụ của chúng tôi):
class Product { constructor(name, price) { this.name = name; this.price = price; } }
OrderItem sẽ là một đối tượng đơn giản chứa sản phẩm và số lượng, như bên dưới:
class OrderItem { constructor(product, count) { this.product = product, this.count = count } }
Tệp OrderList sẽ chứa logic để làm việc với một mảng orderItems.
class OrderList { constructor(items) { this.items = items; } }
Và, main.js chỉ là tệp có thể chứa một số logic ban đầu, có thể nhập OrderList và thực hiện điều gì đó với danh sách này.
import { OrderItem } from './OrderItem'; import { OrderList } from './OrderList'; import { Product } from './Product'; const samsung = new Product('Samsung', 200); const apple = new Product('Apple', 300); const lg = new Product('Lg', 150); const samsungOrder = new OrderItem(samsung, 2); const appleOrder = new OrderItem(samsung, 3); const lgOrder = new OrderItem(samsung, 4); const orderList = new OrderList([samsungOrder, appleOrder, lgOrder]);
Phương thức tính tổng tổng nên được tạo ở đâu? Có ít nhất 2 tệp, và mỗi tệp chúng ta có thể sử dụng cho mục đích này, phải không? Nhưng nơi nào sẽ tốt hơn cho mục tiêu của chúng ta?
Hãy nghĩ về main.js.
Chúng ta có thể viết một cái gì đó như:
const totalSum = orderList.reduce((res, order) => { return res + order.product.price * order.count }, 0)
Nó sẽ hoạt động. Tuy nhiên, tệp orderItem chứa dữ liệu không có phương thức, tệp orderList cũng chứa dữ liệu không có phương thức và tệp chính chứa một phương thức hoạt động với các mục đơn hàng và danh sách đơn hàng.
Nghe có vẻ không ổn. Nếu chúng ta muốn thêm nhiều logic hơn hoạt động với các đơn đặt hàng bằng cách nào đó, chúng ta cũng sẽ đặt nó vào tệp chính chứ? Và, sau một thời gian, tệp chính của chúng ta sẽ có rất nhiều logic khác nhau, cho hàng nghìn dòng mã, điều này thực sự tồi tệ. Phản vật chất này được gọi là đối tượng God , trong đó 1 tệp chứa tất cả.
Nó nên như thế nào nếu chúng ta muốn sử dụng phương pháp tiếp cận chuyên gia thông tin ? Hãy thử lặp lại:
Tất cả các phương thức làm việc với dữ liệu (biến, trường), phải ở cùng một nơi mà dữ liệu (biến hoặc trường) tồn tại.
Điều này có nghĩa là: orderItem phải chứa logic có thể tính tổng cho một mục cụ thể:
class OrderItem { constructor(product, count) { this.product = product, this.count = count } getTotalPrice() { return this.product.price * this.count; } }
Và orderList phải chứa logic có thể tính tổng cộng cho tất cả các mục đặt hàng:
class OrderList { constructor(items) { this.items = items; } getTotalPrice() { return this.items.reduce((res, item) => { return res + item.getTotalPrice(); }, 0); } }
Và, tệp chính của chúng tôi sẽ đơn giản và sẽ không chứa logic cho chức năng đó; nó sẽ càng đơn giản càng tốt (ngoại trừ nhiều nhập khẩu, chúng tôi sẽ sớm loại bỏ).
Vì vậy, bất kỳ logic nào, chỉ liên quan đến một mục đơn đặt hàng, nên được đặt vào orderItem. Nếu một cái gì đó tương đối hoạt động với một tập hợp orderItems, chúng ta nên đặt logic đó vào orderItems.
Tệp chính của chúng tôi chỉ nên là một điểm nhập; thực hiện một số chuẩn bị và nhập khẩu, và kết nối một số logic với những người khác.
Sự tách biệt này mang lại cho chúng ta một số lượng nhỏ sự phụ thuộc giữa các thành phần mã và đó là lý do tại sao mã của chúng ta dễ bảo trì hơn nhiều.
Không phải lúc nào chúng ta cũng sử dụng nguyên tắc này trong dự án của mình, nhưng đó là một nguyên tắc thực sự tốt. Và nếu bạn có thể sử dụng nó, bạn nên làm điều đó.
Người sáng tạo
Trong ví dụ trước của chúng tôi, chúng tôi có 4 tệp: Main, OrderList, OrderItem và Product. Chuyên gia thông tin nói rằng các phương pháp nên ở đâu: ở cùng một nơi có dữ liệu.
Nhưng câu hỏi đặt ra là: đối tượng nên được tạo ra bởi ai và ở đâu? Ai sẽ tạo orderList, ai sẽ tạo orderItem, ai sẽ tạo Sản phẩm?
Creator nói rằng mỗi đối tượng (lớp) chỉ nên được tạo ra ở nơi mà nó sẽ được sử dụng. Hãy nhớ ví dụ của chúng tôi trong tệp chính với nhiều lần nhập? Hãy kiểm tra:
import { OrderItem } from './OrderItem'; import { OrderList } from './OrderList'; import { Product } from './Product'; const samsung = new Product('Samsung', 200); const apple = new Product('Apple', 300); const lg = new Product('Lg', 150); const samsungOrder = new OrderItem(samsung, 2); const appleOrder = new OrderItem(samsung, 3); const lgOrder = new OrderItem(samsung, 4); const orderList = new OrderList([samsungOrder, appleOrder, lgOrder]); const totalSum = orderList.getTotalPrice();
Như chúng ta có thể thấy, hầu hết tất cả các lần nhập và sáng tạo đối tượng đều có trong main.js.
Nhưng, hãy nghĩ xem nó thực sự được sử dụng ở đâu và ai.
Sản phẩm chỉ được sử dụng trong OrderItem. OrderItem chỉ được sử dụng trong OrderList. OrderList được sử dụng trên Main. Nó trông như thế này:
Main → OrderList → OrderItem → Prodcut
Nhưng nếu Main chỉ sử dụng OrderList, tại sao chúng ta lại tạo OrderItem trong Main? Tại sao chúng tôi cũng tạo một Sản phẩm ở đây? Hiện tại, Main.js của chúng tôi tạo (và nhập) hầu hết mọi thứ. Thật tệ.
Theo nguyên tắc Creator , chúng ta chỉ nên tạo các đối tượng ở những nơi mà các đối tượng này được sử dụng. Hãy tưởng tượng rằng, bằng cách sử dụng ứng dụng của chúng tôi, chúng tôi đã thêm sản phẩm vào giỏ hàng. Đây là những gì nó có thể trông như thế này:
Main.js: Chúng tôi chỉ tạo (và nhập) OrderList tại đây:
import { OrderList } from './OrderList'; const cartProducts = [{ name: 'Samsung', price: 200, count: 2 }, { name: 'Apple', price: 300, count: 3 }, {name: 'Lg', price: 150, count: 4 }]; const orderList = new OrderList(cartProducts); const totalPrice = orderList.getTotalPrice();
OrderList.js: Chúng tôi chỉ tạo (và nhập) OrderItem tại đây:
import { OrderItem } from './OrderItem'; class OrderList { constructor(items) { this.items = items.map(item => new OrderItem(item)); } getTotalPrice() { return this.items.reduce((res, item) => { return res + item.getPrice(); }, 0); } }
OrderItem.js: Chúng tôi chỉ tạo (và nhập) Sản phẩm tại đây:
import { Product } from './Product'; class OrderItem { constructor(item) { this.product = new Product(item.name, item.price); this.count = item.count; } }
Product.js:
class Product { constructor(name, price) { this.name = name; this.price = price; } }
Chúng tôi có một phụ thuộc đơn giản:
Chính → Danh sách đặt hàng → Mục đặt hàng → Sản phẩm
Và bây giờ, mỗi đối tượng chỉ tạo ra ở nơi đó, nơi nó được sử dụng. Đó là những gì nguyên tắc Tạo hóa nói.
Tôi hy vọng phần giới thiệu này sẽ hữu ích cho bạn và trong loạt bài tiếp theo về GRASP, chúng tôi sẽ đề cập đến các nguyên tắc khác.
Ảnh của trên