Domain Driven Design (DDD) là một phương pháp thiết kế phần mềm dựa trên nền tảng của mô hình miền (domain model). Mục tiêu chính của DDD là tạo ra phần mềm hiệu quả, bền vững và dễ bảo trì bằng cách tập trung vào việc mô tả và xử lý các Domain phức tạp (Banking, Finance,...). DDD giúp chúng ta tập trung vào nghiệp vụ (Domain) cũng như định nghĩa và sắp xếp source code hiệu quả, giúp giảm thiểu sự phức tạp và tăng khả năng mở rộng của hệ thống bằng các phân tách các lớp trong hệ thống (Presentation, Application, Domain, Infrastructure). Tuy nhiên, để triển khai được DDD một cách hiệu quả, việc xác định rõ nghiệp vụ là vô cùng quan trọng.
1. DDD và Clean Architecture
Trong DDD, có 4 lớp chính:
- Presentation: Chứa các UI, API, giao diện người dùng. Lớp này phụ thuộc vào Application layer.
- Application: Chứa các business logic. Lớp này phụ thuộc vào Domain layer.
- Domain: Chứa các domain model. Đây là trung tâm của hệ thống, chứa các thực thể, trạng thái, logic nghiệp vụ.
- Infrastructure: Chứa các technical detail như database, messaging, caching, ... Lớp này phụ thuộc vào Domain layer.
So sánh với Clean Architecture:
- Presentation layer tương ứng với UI layer
- Application layer tương ứng với Application Business Logic layer
- Domain layer tương ứng với Enterprise Business Logic layer
- Infrastructure layer tương ứng với Infrastructure layer
Cả DDD và Clean Architecture này đều có chung mục đích là tách biệt các lớp trong hệ thống, giảm phụ thuộc giữa các layer, dễ dàng thay đổi và mở rộng. Tuy nhiên, DDD nhấn mạnh vào việc mô hình hóa miền nghiệp vụ (Domain model), trong khi Clean Architecture thì có phạm vi rộng hơn.
Nói chung, DDD là principal, approach, mindset, trong khi Clean Architecture là một kiến trúc phần mềm (Architectural pattern).
Trong DDD và Clean Architecture, các lớp được thiết kế theo nguyên tắc phụ thuộc ngược (dependency inversion). Có nghĩa là chúng ta có thể tháo Domain layer và lắp cho những thằng khác bên ngoài. Cụ thể:
- Presentation layer phụ thuộc vào Application layer
- Application layer phụ thuộc vào Domain layer
- Domain layer KHÔNG phụ thuộc vào các lớp khác
- Infrastructure layer phụ thuộc vào Domain layer
Như vậy:
- Application layer biết về Domain layer, nhưng Domain layer KHÔNG biết về Application layer.
- Presentation layer biết về Application layer, nhưng Application layer KHÔNG biết về Presentation layer.
- Infrastructure layer biết về Domain layer, nhưng Domain layer KHÔNG biết về Infrastructure layer.
Mối quan hệ giữa các lớp là một chiều, từ trên xuống dưới. Điều này giúp:
- Giảm sự phụ thuộc giữa các lớp, khi thay đổi một lớp thì các lớp phụ thuộc vào nó không bị ảnh hưởng.
- Domain layer trở thành trung tâm, không phụ thuộc bất kỳ lớp nào khác nên rất dễ mở rộng và thay đổi.
- Dễ dàng thay thế các lớp ở ngoài (như DB, interface, ...) mà không ảnh hưởng đến Domain.
Nói chung, các lớp ở ngoài cùng (Presentation, Infrastructure) phụ thuộc vào các lớp bên trong (Application, Domain), nhưng không ngược lại. Đây có thể hiểu nôm na như là nguyên tắc Dependency Inversion trong Clean Architecture và Domain Driven Design.
2. Các khái niệm trong DDD
Một số khái niệm quan trọng trong DDD bao gồm:
-
Ubiquitous Language: Đây là một ngôn ngữ chung mà tất cả các thành viên trong team (dev, BA, PO, v.v.) sử dụng để giao tiếp. Nó giúp đảm bảo rằng mọi người hiểu đúng ý nghĩa của các thuật ngữ trong Domain.
Ví dụ: Trong một hệ thống quản lý đơn hàng, "Order", "Customer" và "Product" là các thuật ngữ thuộc Ubiquitous Language mà mọi người trong đội sử dụng để nói về hệ thống.
-
Domain: Là domain (nghiệp vụ) chính của hệ thống. Nó bao gồm các quy tắc, chức năng và đối tượng liên quan đến domain đó, trong domain chính có thể bao gồm các sub domain
Ví dụ: Trong một hệ thống quản lý đơn hàng, Domain chính là việc quản lý, xử lý và theo dõi các đơn hàng.
-
Sub Domain: Là 1 domain con hoạt động bên trong một domain lớn hơn. Chúng giúp chia nhỏ Domain thành các phần nhỏ hơn, dễ quản lý hơn.
Ví dụ: Trong một hệ thống quản lý đơn hàng, các Sub Domain có thể bao gồm quản lý sản phẩm, quản lý khách hàng, quản lý thanh toán, v.v.
-
Bounded Context: Là một ranh giới được định nghĩa để phân tách các Sub Domain khác nhau, giúp giảm thiểu sự phức tạp và tăng khả năng mở rộng của hệ thống. Mỗi Bounded Context đại diện cho một Sub Domain độc lập với những domain model của riêng sub domain đó. Chúng ta sẽ gặp khái niệm này khi nói đến kiến trúc Microservices.
Ví dụ: Trong một hệ thống quản lý đơn hàng, chúng ta có thể chia thành các Bounded Context như "Sales", "Catalog", "Payment" và "Shipping".
-
Entity: Là các thực thể trong hệ thống, có thể như "Product", "Customer", "Order"
-
Value Object: Là các thực thể không có định danh (ID), để bổ sung thông tin cho các Entity, ví dụ như Address, tuy nhiên đối với từng bài toán, ta sẽ có cách định nghĩa các Value object riêng, không phải bài toán nào cũng có Address là 1 Value Object, có thể trong bài toán quản lí địa chỉ, Address lại đóng vai trò là 1 Entity.
-
Aggregate Root: Là một đối tượng trong Aggregate chịu trách nhiệm duy trì tính nhất quán và đúng đắn của dữ liệu. Nó là điểm duy nhất để truy cập và thao tác các thành viên bên trong Aggregate.
Ví dụ: Trong một hệ thống quản lý đơn hàng, "đơn hàng" (Order) có thể là Aggregate Root của Aggregate bao gồm "đơn hàng" và "chi tiết đơn hàng" (OrderLine).
-
Aggregate: Trong DDD, Aggregate là một nhóm các đối tượng (Entities và value objects) được tổ chức lại theo một quy tắc cụ thể. Mỗi Aggregate đảm bảo tính nhất quán và đúng đắn của dữ liệu bên trong nó thông qua các ràng buộc. Aggregate Root là một entity chịu trách nhiệm cho việc duy trì tính consistency của các đối tượng trong Aggregate. Khi giao tiếp giữa các Aggregate, chỉ cho phép giao tiếp thông qua Aggregate root.. Ví dụ Order Aggregate có nhiệm vụ thêm Order, xóa Order, cập nhật Order thông qua Aggregate Root là Order và entity như OrderItem. Aggregate đảm bảo tính nhất quán và đúng đắn của dữ liệu bên trong nó thông qua các ràng buộc. 1 Aggregate chứa 1 Aggregate Root để giao tiếp với các Aggregate Root ở những Aggregate Root khác.
Ví dụ: Trong một hệ thống quản lý đơn hàng, Aggregate có thể bao gồm "đơn hàng" (Order) và "chi tiết đơn hàng" (OrderLine) với "đơn hàng", (OrderItem) với "sản phẩm trong order". Với Order làm Aggregate Root.
Giả sử có 10 thực thể (Entity) trong 1 ứng dụng mua sắm trực tuyến
Customer, Employee, Order, OrderLine, Product, Inventory, Payment, Shipment, Category, Supplier
Dựa trên các thực thể này, chúng ta có thể phân chúng thành các Bounded Context sau:
1. Sales Context: Aggregate: Order (Aggregate Root), OrderItem Aggregate: Customer
2. Catalog Context: Aggregate: Product (Aggregate Root), Category, Supplier
3. Inventory Context: Aggregate: Inventory (Aggregate Root), Product
4. Shipping Context: Aggregate: Shipment (Aggregate Root), Order
5. Payment Context: Aggregate: Payment (Aggregate Root), Order, Customer
6. Human Resources Context: Aggregate: Employee
Trong ví dụ này, chúng ta đã chia các thực thể thành 6 Bounded Context khác nhau dựa trên mối quan hệ và trách nhiệm của chúng. Mỗi Bounded Context chứa một hoặc nhiều Aggregate, với mỗi Aggregate có một Aggregate Root đảm bảo tính nhất quán và đúng đắn của dữ liệu. Lưu ý rằng một số thực thể có thể xuất hiện ở nhiều Bounded Context, như Product và Order, nhưng chúng sẽ có trách nhiệm khác nhau trong mỗi ngữ cảnh.
Trong Domain Driven Design (DDD), Repository là một thành phần thiết kế chịu trách nhiệm cho việc truy xuất và lưu trữ các đối tượng Aggregate Root. Repository giúp giải quyết vấn đề truy xuất dữ liệu và giữa giao tiếp giữa tầng miền (domain) và tầng cơ sở dữ liệu (infrastructure). Các Repository thường được định nghĩa cho từng Aggregate Root và cung cấp các phương thức để truy vấn, thêm, sửa, xóa và lưu trữ các đối tượng Aggregate Root.
3. Ví dụ về Aggregate, Bounded Context và Aggregate Root trong DDD
Ví dụ, giả sử chúng ta có Aggregate Root Order và Product trong hệ thống quản lý đơn hàng. Chúng ta sẽ tạo ra hai Repository tương ứng là OrderRepository và ProductRepository.
OrderRepository interface có thể bao gồm các phương thức sau:
public interface OrderRepository { Order findById(int orderId); List<Order> findByCustomerId(int customerId); void add(Order order); void update(Order order); void remove(int orderId);
}
Để đảm bảo tính nhất quán như mình đã nói, chúng ta sẽ không communicate giữa các child entity, ở đây là OrderItem bằng cách tạo thêm 1 repository cho child entity, thay vào đó, chúng ta sẽ định nghĩa các phương thức ở trong entity Order
public class Order
{ // ... Các thuộc tính và phương thức khác của Order public void AddOrderItem(OrderItem orderItem) { OrderItems.Add(orderItem); } public void RemoveOrderItem(Guid orderItemId) { var orderItem = OrderItems.FirstOrDefault(oi => oi.Id == orderItemId); if (orderItem != null) { OrderItems.Remove(orderItem); } } public void UpdateOrderItem(Guid orderItemId, int newQuantity) { var orderItem = OrderItems.FirstOrDefault(oi => oi.Id == orderItemId); if (orderItem != null) { orderItem.Quantity = newQuantity; } }
}
ProductRepository interface có thể bao gồm các phương thức sau:
public interface ProductRepository { Product findById(int productId); List<Product> findByCategory(int categoryId); void add(Product product); void update(Product product); void remove(int productId);
}
Trong các ứng dụng thực tế, bạn sẽ cần triển khai các Repository này dựa trên công nghệ cơ sở dữ liệu mà bạn sử dụng (ví dụ: SQL, NoSQL, hoặc in-memory storage).
Khi muốn thực hiện các thao tác trên các Aggregate Root, chúng ta sẽ sử dụng các Repository đã được định nghĩa. Cách tiếp cận này giúp mã nguồn dễ hiểu hơn, dễ bảo trì hơn và giúp dễ dàng thay đổi công nghệ cơ sở dữ liệu mà không ảnh hưởng đến miền ứng dụng.