1. Domain Driven Design là gì?
Domain-driven design (DDD) là một cách tiếp cận cho phát triển các phần mềm phức tạp; sự phức tạp ở đây là do nghiệp vụ của lĩnh vực kinh doanh (domain business). Cách tiếp cận của DDD là kết nối chặt chẽ việc cài đặt phần mềm với sự tiến hoá của mô hình kinh doanh:
– Dự án đặt sự chú trọng nhiều cho phần nghiệp vụ chính (core domain) và các logic nghiệp vụ liên quan.
– Mô hình hoá của nghiệp vụ là trọng tâm, nền tảng cho mọi cài đặt phần mềm.
– Tăng cường cộng tác giữa nhóm kỹ thuật( developers) và các chuyên gia nghiệp vụ (domain expert) để xây dựng một mô hình tốt giúp xác định và giải quyết hiệu quả bài toán. wiki
DDD được tác giả Eric Evans đưa ra sau kinh nghiệm khoảng 20 năm tham gia phát triển phần mềm, xây dựng những ứng dụng lớn cho giới tài chính, ngân hàng, bảo hiểm, giao vận quốc tế.. Dựa trên kinh nghiệm thực tiễn, những best practices tích luỹ được, ông đề xuất một phương pháp tiếp cận mới để xây dựng những phần mềm có nghiệp vụ phức tạp mà nhóm phát triển sẽ gặp rất nhiều khó khăn để làm chủ, quản lý rủi ro, phát triển và duy tu lâu dài. Phương pháp của ông được tổng hợp trong cuốn sách xanh nổi tiếng ” Domain Driven design – tackling complexity in the heart of software” từ 2003. Và Phương pháp đã nhanh chóng nhận được sự tán dương của cộng đồng, ngày càng được phát triển và áp dụng.
2. Ubiquitous Language (Ngôn ngữ chung)
Yêu cầu đầu tiên của cách tiếp cận DDD là Ngôn ngữ Chung (UL). DDD nhấn mạnh sự cộng tác, không bó hẹp giữa các lập trình viên với nhau, do đó những người liên quan phải có khả năng trao đổi để cùng hiểu một vấn đề nghiệp vụ.
Thực tế chỉ ra rằng các từ vựng “chuyên ngành” rất dễ bị hiểu sai với người ngoài ngành, thậm chí cùng một thuật ngữ khác nhau theo các vai trò khác nhau, định nghĩa cũng có thể khác nhau.
** Ví dụ ** thực tế trong dự án gần đây, nhóm tôi có nhận được yêu cầu về quản lý các dự án cho khách hàng. Và khi thảo luận team tưởng tượng rằng dự án sẽ có các thuộc tính bắt đầu, kết thúc, giá thành, nhân viên, rồi để hoàn thành sẽ có nhiều nhiệm vụ gán cho các thành viên trong đội. Nhóm tưởng tượng đến những trello, jira, dotproject. Nhưng sau này khi nhận được sample từ khách hàng mới nhận ra Project chỉ tương đương với một bản ghi exel tương ứng với một code từ hoá đơn gán cho một người phụ trách.
Vậy làm thế nào để có ngôn ngữ chung để ai tham gia dự án cũng thống nhất một cách hiểu, có thể chia sẻ với nhân viên kinh doanh hay nhân viên kỹ thuật. Evan gợi ý rằng nó là tài liệu để mọi người cùng hiểu.
3. Kiến trúc phân lớp
Khi các đoạn code liên quan đến nghiệp vụ được trộn lẫn giữa các tầng lại với nhau, nó trở nên vô cùng khó khăn cho việc đọc cũng như suy nghĩ về chúng. Các thay đổi ở giao diện người dùng cũng có thể thực sự thay đổi cả logic nghiệp vụ. Để thay đổi logic nghiệp vụ có thể yêu cầu tới truy vết tỉ mỉ các đoạn mã của giao diện người dùng, CSDL, hoặc các thành phần khác của chương trình. Mô hình phát triển hướng đối tượng trở nên phi thực tế. Do đó, hãy phân chia một chương trình phức tạp thành các LỚP. Phát triển một thiết kế cho mỗi Layer(Lớp) để chúng trở nên gắn kết và chỉ phụ thuộc vào các tầng bên dưới. Dưới đây là giải pháp kiến trúc chung cho DDD.
Ở đây mô hình DDD vẫn giữ lại những ưu điểm của mô hình kiến trúc phân lớp (Layered Archiecture) để đảm bảo nguyên lý Seperation of Concerns. Các phần logic xử lý khác nhau sẽ được cô lập ra khỏi các phần khác làm tăng tính Loose Coupling của ứng dụng và tính dễ đọc và dễ bảo trì cũng như ứng dụng khi có thay đổi logic của từng layer thì không ảnh hướng đến các layer khác.
===========================================
User Interface: Chịu trách nhiệm trình bày thông tin tới người sử dụng và thông dịch lệnh của người dùng. Có thể hiểu là các sự kiện xảy ra trên giao diện khi được trigger sẽ được dịch thành lệnh và xử lý ở các tầng dưới.
Applicatioin Layer: Tầng này được thiết kế khá mỏng (ít xử lý logic) phối hợp các hoạt động của ứng dụng. Nó không chứa logic nghiệp vụ. Nó không lưu giữ trạng thái của các đối tượng nghiệp vụ nhưng nó có thể giữ trạng thái một tiến trình của ứng dụng. Chúng ta có thể hình dung phần này gần giống với các Controller trong mô hình MVC chỉ làm nhiệm vụ chuyển tiếp các task đến nơi cần xử lý.
Domain Layer: Tầng này chứa thông tin về các lĩnh vực. Đây chính là trái tim của phần mềm. Trạng thái của đối tượng nghiệp vụ được giữ tại đây.
Infrastructure Layer: Tầng này đóng vai trò như một thư viện hỗ trợ cho tất cả các tầng còn lại. Nó cung cấp thông tin liên lạc giữa các lớp, cung cấp chức năng lưu trữ các đối tượng nghiệp vụ, chứa các thư viện hỗ trợ cho tầng giao diện người dùng...
===========================================
Đến đây thì chúng ta sẽ thấy kiến trúc của DDD tuy mới nhìn có vẻ lạ nhưng chỉ đơn giản là nó tùy biến lại mô hình kiến trúc 3 lớp (3-tier architecture) cho linh hoạt hơn. Tính linh hoạt này được tạo ra từ hệ quả của việc tái tổ chức lại các layer từ mô hình ba lớp, nó thể hiện ở data flow và control flow giữa 2 mô hình.
4. Basic element - những thành phần cơ bản
4.1 Entity
Trong DDD, việc quan trọng cần phải làm là mô hình hóa các domain để cả dev lẫn domain expert đều nắm được. Và để mô hình hóa thì thành phần không thể thiếu là các entity (đối tượng). Tất cả các domain đều phải có đối tượng cụ thể và khái niệm về entity chắc cũng quá gần gũi với chúng ta. Ai mà lại không biết về OOP chứ. Entity trong DDD có một chút khác biệt là nó phải được định danh (có ID) và định danh phải bất biến và duy nhất trong toàn bộ hệ thống. Việc phân biệt các thực thể là rất quan trọng. Việc chia hệ thống dựa theo các domain sẽ tạo ra việc đối tượng trong nhiều domain là thực chất là một đối tượng. Việc gắn ID sẽ giúp cho xác định đối tượng có là một hay không trở nên đơn giản hơn. Chúng ta không thể nào phân biệt dựa trên các thuộc tính của đối tượng đó mà cần phải có thuộc tính đặc thù nhất gắn liền với đối tượng.
Bên cạnh đó, vì cùng một đối tượng có thể nằm ở nhiều domain khác nhau và các domain này độc lập với nhau nên entity cần thiết phải chứa logic của riêng nó để có thể sử dụng trên nhiều domain và mỗi domain không cần phải quan tâm đến những logic đó. Điều này đảm bảo cho tính nhất quán của hệ thống và giảm bớt những xử lý dư thừa. Thêm một điều cần lưu ý, entity cũng cần phải có life cycle (creation and deletion) trong chính bản thân nó. Vì việc được sử dụng trên nhiều domain độc lập sẽ không đảm bảo việc tạo hoặc xóa bỏ đối tượng đó đúng cách. Chúng ta thường hay thiết kế theo anemic model nghĩa là đặt các logic này trong các Service, Util, Helper thay vì đặt ở trong entity... nhưng với DDD, chúng ta không làm như thế.
Để dễ hình dung, hãy nhìn vào thẻ visa của bạn, nó là một entity. Nó có thể được dùng trong nhiều domain khác nhau như thanh toán online, chuyển tiền qua tài khoản khác... Và chắc chắn rằng thẻ visa của bạn phải có mã thẻ (hay số tài khoản) là duy nhất, nó không được phép trùng với bất kì cái nào khác và trên hết nó không thể thay đổi được. Trên thẻ, nó có những thông tin về ngày khả dụng, ngày hết hạn, tương ứng với life cycle của nó (creation và deletion). Những thông tin này thuộc về thẻ và logic xử lý hợp lệ hay hết hạn cũng sẽ dựa trên chính thẻ đó. Nếu không đề cập tới các service về quản lý cấp cao, thì không có bất cứ service nào có thể chứa logic về thay đổi kì hạn của thẻ.
Chốt, entity trong DDD là một đối tượng:
Có định danh bất biến và duy nhất
Chứa life cycle: creation và deletion
Nên chứa các logic của riêng nó thay vì thiết kế theo anemic model
4.2 Value Object
Trong Domain, có những lúc chúng ta cần có các thuộc tính của các phần tử trong Domain. Khi đó chúng ta không cần quan tâm đó là đối tượng nào mà chúng ta chỉ quan tâm đến các thuộc tính của chúng. Các đối tượng mà dùng để miêu tả các thông tin cố đinh trong domain mà không cần định danh thì ta gọi chúng là Value Object
Về cơ bản thì Value Object và Entity là giống nhau, chỉ khác nhau là Entity thì có định danh còn Value Object thì không. Đối tượng Entity là mutable (có thể thay đổi) trong khi Value Object là immutable (bất biến - readonly) tức là nó được tạo bởi constructor và sẽ không bao giờ thay đổi . Do đó, khi bạn muốn cập nhập thông tin cho đối tượng thì bạn phải tạo một đối tượng mới chứ không phải thay đổi giá trị của đối tượng cũ. Nhờ không có định danh nên Value Object có thể được tạo và huỷ dễ dàng. Cũng nhờ không có định danh nên Value Object có thể được dùng chung và nó có thể chứa các Value Object khác hoặc một Entity.
Trong lập trình thì chúng ta gặp rất nhiều Value Object ví dụ như trong Java chúng ta có kiểu DateTime, String, Int – chúng đều là các Value Object, hoặc chúng ta có một đối tượng quảng cáo Ads có một Value Object là status mà nó có các giá trị là ACTIVE, INACTIVE, DELETED.
Trong Domain, khi thiết kế thì chúng ta cần phải định nghĩa rõ ràng xem những đối tượng nào là Entity, và những đối tượng nào là Value Object. Nó hoàn toàn phụ thuộc vào thiết kế bài toán và nghiệp vụ của phần mềm. Ví dụ trong một đối tượng User thì chúng ta có thể coi address là một Value Object nhưng trong một Domain khác chúng ta cần quản lý địa chỉ từng khu vực một thì lục này chúng ta có thể coi Address là một Entity.
4.3 Aggregates
Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it's useful to treat the order (together with its line items) as a single aggregate. - Martin Fowler
Đây có lẽ là khái niệm hay nhất và rối rắm nhất trong DDD. "Một Aggregate là một nhóm các đối tượng, nhóm này có thể được xem như là một đơn vị thống nhất". Một nhóm mà lại xem như là một đơn vị, thật khó hiểu. Để nắm được khái niệm Aggregate một cách dễ dàng nhất hãy nghĩ về hình ảnh chùm nho. Một chùm nho thì có nhiều trái nho được nối với nhau trên một cuống nho và nối với thân cây nho thông qua gốc của cuống nho đó. Và trên cây nho thì có rất nhiều chùm nho. Tưởng tượng rất đơn giản, chúng ta bắt đầu với aggregate.
Một aggregate bao gồm các entity như chùm nho thì có nhiều trái nho. Các trái nho trong một chùm có thể kết nối lẫn nhau nhưng nếu muốn tạo liên kết đến trái nho ở chùm khác thì bắt buộc nó phải thông qua cái gốc của chùm nho, đi vào thân và đến gốc của chùm nho khác rồi mới tới được trái nho của chùm khác. Tương tự, Các entity trong nội bộ aggregate có thể tự do tham chiếu đến nhau tuy nhiêu muốn tham chiếu đến đối tượng nằm ở aggregate khác thì thì nó phải thông qua gốc của aggregate (aggregate root). Điều này giúp giảm bớt sự phụ thuộc giữa các entity trong hệ thống. Thay vì chúng phải kết nối lẫn nhau thì bây giờ chúng chỉ cần liên kết thông qua các aggregate root. Giảm đi vô số liên kết tức là giảm đi vô số phụ thuộc. Điều này giúp tăng khả năng linh hoạt của hệ thống, thứ mà đang trở thành yêu cầu hàng đầu trong phát triển ứng dụng ngày nay. Một khi cắt gốc chùm nho, không còn cách nào để giữ trái nho lại trên cây cả. Tuy nhiên thì các chùm nho khác trên cây vẫn sống bình thường chỉ là không thể nào tham chiếu đến các trái nho trong chùm nho đã cắt được thôi.
Bên cạnh đó, để các entity có thể tham chiếu đên nhau trong aggregate thì nhất thiết phải có logic xử lý nằm ở aggregate. Cái trái nho muốn nối với nhau thì phải thông qua cuống nho đó thôi. Nên phải chú ý rằng trong một aggregate, phải đảm bảo có đầy đủ các logic liên quan đến tất cả entity chứa trong nó. Từ đó các entity mới có thể giao tiếp với nhau. Và những aggregate khác muốn tác động đến các entity này chỉ cần sử dụng các logic đó mà thôi, không nhất thiết phải tạo thêm logic chỉ đích danh chính entity đó. Tức là chỉ cần giao tiếp với aggregate là có thể giao tiếp với tất cả các entity có trong aggerate đó.
đúc kết lại, aggerate là:
Một tập hợp các thực thể
Các xử lý đề thông qua root entity nên có thể xem aggerate là một đơn vị thống nhất. Lưu ý: aggerate phải chứa các xử lý logic liên qua đến tất cả các entity
4.4 Domain Services
Khi mô hình hóa bài toán thực tế ta cần biểu diễn thực tiễn qua các khái niệm, nhưng Value Object hay Entity thì không đủ, ví dụ để biểu diễn các operations, business policy, process. Ở đây chúng nên được biểu diễn là các service.
Thiết kế một service thì nên là stateless, nghĩa là service sau khi phục vụ xong client thì không nên lưu trữ lịch sử giao dịch phục vụ cho kết quả lần tới, xong thì thôi. Một Service trong DDD là một cấu thành quan trọng của model nên cũng cần được làm rõ trong UL. Một vấn đề lưu ý nữa là phân chia nhiệm vụ cho service ở các tầng khác nhau thì khác nhau, ví như service ở infra có thể lo những dịch vụ hạ tầng về liên lạc, thông báo lỗi, truy xuất cơ sở dữ liệu.. không chứa thông tin về nghiệp vụ. Service ở domain phải mang thông tin về xử lý nghiệp vụ, còn Service ở application có thể kết hợp gọi service ở domain và kết hợp xử lý lỗi, cảnh báo lỗi từ Infra cung cấp, để hoàn thành một business use cases.
5. Các Khó khăn khi sử dụng DDD
DDD là một hướng tiếp cận giải quyết cho những phần mềm lớn và phức tạp, vì thế nó áp dụng nhiều design patterns và các best practices. Việc làm chủ những khái niệm này là không dễ và đòi hỏi nhiều kinh nghiệm. Ngoài ra cả team đều phải hiểu và tuân theo các rule của DDD.
DDD đòi hỏi cao trong sự cộng tác cao của nhóm phát triển với các chuyên gia về nghiệp vụ. Nếu không phải là một chính sách, quyết tâm của công ty thì cũng gặp nhiều khó khăn để áp dụng.
Tài liệu Tham Khảo
Source tham khảo DDD- eShopOnWeb
https://github.com/dotnet-architecture/eShopOnWeb
Link sách của eric evans: Domain-Driven Design - Tackling Complexity in the Heart of Software
http://93.174.95.29/main/186000/11edcb29974052333fcf77d15eb084c5/Eric Evans - Domain-Driven Design - Tackling Complexity in the Heart of Software-Addison Wesley (2003).pdf
Link sách martin fowler: Patterns of Enterprise Application Architecture:
http://93.174.95.29/main/24000/33b930f8bbc30aa5faffa32c08da9fa1/Martin Fowler - Patterns of Enterprise Application Architecture-Addison-Wesley Professional (2002).pdf
https://viblo.asia/p/domain-driven-design-phan-1-mrDGMOExkzL
https://vngeeks.com/rich-model-vs-anemic-model/
Thảo luận