Trải nghiệm chuyên sâu Domain Driven Design

Trải nghiệm chuyên sâu Domain Driven Design

Xem nhanh

Bài viết cực dài, mình đã nghiên cứu nhiều năm để viết ra một bài này. Hi vọng các bạn sẽ nắm vững tư tưởng DDD và tiến xa hơn trong sự nghiệp. Bạn muốn đọc bài này hay một cuốn sách English 1000 trang.

Bạn cũng đừng có gắng hiểu bài này trong 1 vài giờ hoặc một vài ngày. Đừng bắt ép bản thân phải làm điều đó. Vì việc học là một hành trình lâu dài. Hãy trải nghiệm nó.

Một phút thật lòng, Domain Driven Design (DDD) là một trong những kiến thức khó và hay nhất từ trước đến giờ (đối với bản thân mình). Mình đã từng trãi nghiệm nó trong dự án thực tế ở công ty từ 4 năm trước (hiện tại là 6/2024). Anh technical lead của mình đã bày đầu cho anh em về DDD. Mình cứ tà tà với nó cho tới khoảng hơn 1 năm nay. Và mình cứ tưởng đã thấm nhuần tư tưởng của DDD rồi. Nhưng thực tế là có những thứ mình vẫn còn chưa rõ. Càng đọc sách, càng đọc các bài blog trên mạng thì mình càng lú. Dẫn tới tẩu hỏa nhập ma nhiều lần.

Cho đến 1 ngày, tự nhiên mình lại ngộ ra các chiêu thức của Domain Driven Design. Mình đã vở òa vì sung sướng. Thật ra không phải tự nhiên đâu, mà mình cày nát mấy cuốn sách đó.

(Không biết những giác ngộ của mình có đúng không. Nếu vẫn còn sai sót hi vọng các bạn đọc có thể comment và giải đáp giúp mình).

Và mình quyết định viết một bài...tất tần tật về Domain Driven Design.

Hiện tại có khá nhiều bài viết về DDD, nhưng rất ít trong số đó truyền tải đúng đắn những tư tưởng của DDD. Hoặc nói sơ sài lắm. Các bạn nên cân nhắc khi đọc cái bài viết đó.

Vì theo mình DDD là một hệ tư tưởng, một triết lý, cách tiếp cận trong phát triển phần mềm. Nó chẳng có một implement nào cụ thể. Nên rất khó xác định, cái gì là đúng, cái gì là sai.

Đơn giản là, cái gì được số đông cộng đồng DDD sử dụng, thì cái đó là đúng.

Trước khi bắt đầu. Để hiểu (hay apply được vào doanh nghiệp) DDD sẽ có 2 thành phần chính và to nhất là Strategic DesignTactical Design. Mình trích nguyên văn trong cuốn sách Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy như sau: The strategic aspect of DDD deals with answering the questions of “what?” and “why?”—what software we are building and why we are building it. The tactical part is all about the “how”—how each component is implemented.

Hiểu đơn giản, để thực hiện được Strategic Design, doanh nghiệp của bạn phải làm sao đó để tìm hiểu, phân tích và design được một high level - view của domain doanh nghiệp của mình. Hiểu được lợi thế cạnh tranh, thế mạnh, sự khác biệt của mình so với các đối thủ. Để làm được điều này, chúng ta có thể dựa trên các công cụ và thuật ngữ của DDD: subdomain, bounded context, event storming, context map, ... Từ đó chúng ta sẽ trả lời được câu hỏi. Doanh nghiệp của chúng ta (software cần design) làm cái gì, làm được cái gì (what), và tại sao phải làm những cái đó (why).

Giai đoạn trên sẽ không có một dòng code nào hết. Chỉ đơn thuần là phân tích về domain phức tạp của chính mình. Rõ ràng một điều, nếu không hiểu về domain một cách tường tận nhất, doanh nghiệp của bạn sẽ chẳng thể design ra được một sản phẩm tốt nhất cho khách hàng.

Đọc những dòng trên, có nhiều bạn sẽ cảm thấy khó hiểu lắm. Thật ra mình cũng vậy. Ở doanh nghiệp của bạn (kể cả mình), bạn sẽ rất ít cơ hội để làm việc này (trừ khi bạn là founder hoặc những người đi với founder ngay từ đầu, hoặc có level cao, chức vụ cao như manager, leader, SA, ...). Và theo mình Strategic Design là một cái gì khá là khó, dẫn tới nhiều người sẽ cảm thấy nhàm chán (ý kiến cá nhân). Và dẫn đến ít người quan tâm tới nó. Nhưng...nó là một trong 2 thành phần quan trọng nhất trong DDD.

Nếu các bạn đọc có những kinh nghiệm quý báu nào về Strategic Design có thể comment để chia sẻ cho mình và các bạn khác để có thể hiểu thêm về nó. Mình không dám múa rìu qua mắt thợ.

Còn về Tactical Design, nó đơn giản là dựa trên kết quả của Strategic Design để từ đó design ra những thứ nhỏ hơn - low-level. Lúc này là lúc các bạn sẽ design ra được các business logics, những building blocks trong tactical như: Value object, Entity, Aggregate, Service, .... Nơi mà các bạn (devs, ...) sẽ chắn chắn được tham gia vào (hoặc được transfer từ các anh chị level cao hơn như tech leads, SA, ...) để từ đó bắt đầu code.

Và trong bài viết này, mình sẽ tập trung chủ yếu vào Tactical Design (nơi mình tự tin hơn). Strategic Design mình chưa tự tin để trình bày nhiều cho các bạn. Các bạn có thể tham khảo một số cuốn sách sau nếu muốn đi sâu hơn về nó. Trong mấy cuốn sách này còn lấy ví dụ, và trãi qua quá trình phân tích domain luôn. Đọc cũng khá cực đó.

  • Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy by Vlad Khononov
  • Domain-Driven Design: Tackling Complexity in the Heart of Softwarw by Eric Evans
  • Implementing Domain-Driven Design by Vaughn Vernon

Bạn dành tầm vài tháng đốt 3 cuốn sách đó uống, đảm bảo là bạn sẽ giác ngộ. Nói chứ đọc review mấy cuốn sách đó và chọn cuốn ngon nhất đọc nha.

Thôi, thật lòng dài rồi, mình bắt đầu thôi =]].

Hiểu về domain

Domain là nghiệp vụ.

Chính nó, domain về ngân hàng là những nghiệp vụ về ngân hàng như: Tài khoản, thẻ tín dụng, cho vay, nợ, thẻ ATM, .... Domain về thương mại điện tử thì xoay quanh giỏ hàng, mua hàng, sản phẩm, thanh toán, shipping, tồn kho, ....

Túm lại domain chính là nghiệp vụ ở một lĩnh vực nào đó.

Nói đơn giản vậy là các bạn sẽ hiểu rồi.

À, trong một domain lớn, người ta tách thành nhiều domain nhỏ hơn thì gọi nó là subdomain. Dễ hiểu ha.

Đoạn này là bonus thêm: Trên thực tế, để có thể tìm ra các subdomain và phân chia chúng thuộc loại nào thì doanh nghiệp phải trãi qua một quá trình dài để phân tích về domain của mình. Cái này nó còn liên quan đến việc tạo ra sự khác biệt và lợi thế cạnh tranh trên thị trường (hơi phức tạp). Vì vậy sau khi tìm hiểu, phân tích các kiểu thì subdomain có thể chia làm 3 loại sau:

  • Core subdomains
  • Generic subdomains
  • Supporting subdomains

Mấy ông subdomains đó các bạn chịu khó đọc sách ở trên tiếp nha.

Khi nói về domain, có một sự thật như thế này:

  • Khách hàng chỉ quan tâm tới domain (nghiệp vụ)
  • Còn devs team thì thường hay suy nghĩ về technical. Như apply framework gì, công nghiệp gì, thiết kế DB như nào, quan hệ ra sao (1-n, n-n, ...).

Cho nên đó là một trong những vấn đề làm cho các dự án phần mềm thất bại, phá sản, .... Hoặc đơn giản là khi release lại không đúng ý khách hàng, dẫn tới sửa spec lui tới. Đặc biệt là khi làm outsourcing. Đó là lý do sinh ra các ông như Business analytic, Bridge Software Engineer (Kỹ sư cầu nối).

Business rule hay business logic là gì?

Ví dụ!

Team bạn nhận một dự án từ khách hàng (một công ty truyền thông ở Đông Lào).

Và yêu cầu của họ là tạo một trang báo điện tử (giống 24h hay vnexpress ấy các bạn). Và khi khách hàng truyền tải requirement về cho các bạn. Họ sẽ có một số lời nói khá quen thuộc như sau (mình nói về context User):

  • Mỗi user chỉ có 1 email duy nhất và không trùng với user khác
  • Khi tạo user thì mặc định sẽ có role là Subscriber.
  • Có 3 system roles: Admin, Author, Subscriber.
  • User admin có thể tạo được Role.
  • User admin có quyền tạo thêm roles.
  • Role thì không được trùng tên (unique) với nhau.
  • Không được chỉnh sửa system role.
  • Role name chỉ được chứa ký tự a-z, A-Z, 0-9 và _.
  • Role name chỉ có độ dài tối thiểu là 3 ký tự, tối đa 100 ký tự.

Hoặc trong một ứng dụng Food Ordering:

  • Khi mới tạo một đơn hàng (order) thì trạng thái (status) của nó sẽ là pending.
  • Total price của một đơn hàng không được nhỏ hơn 0.
  • Khi payment (thanh toán) thất bại, trạng thái cuối cùng của đơn hàng sẽ là canceled.
  • Khi payment thành công và hàng trong kho còn đủ số lượng cho đơn hàng thì trạng thái đơn hàng sẽ là approved.

Đấy, tất cả các gạch đầu dòng trên, là business logics! Dễ hiểu phải không các bạn.

Và có một điều các bạn phải lưu ý:

  • Business logics là cái thử rất dễ thay đổi và mở rộng. Vì sản phẩm của các bạn phải đáp ứng được nhu cầu của khách hàng (khách hàng là thượng đế lại còn khó tính). Càng phát triển thì nhu cầu của khách hàng càng thay đổi và mở rộng nhiều hơn.

Quan trọng hơn! Trong DDD, các business logic sẽ được đặt trong core domain layer. Cụ thể là trong các value object, entity, aggregate, domain service. Các bạn sẽ được tìm hiểu kĩ càng hơn về các khái niệm mình vừa đề cập.

Mình cùng đi tiếp thôi.

Vấn đề khi project lớn dần?

Trước tiên chắc mình nói về mặt codebase thôi ha.

  • Khi codebase lớn dần, có một điều chắc chắn: business logics sẽ lớn dần theo và ngày càng trở nên phức tạp.
  • Business logic bị phân tán khắp nơi trong các layer: controller cũng có, service cũng có, model cũng có, thậm chí trên view và tầng database cũng chứa luôn business logics. (Mình đã từng thấy vấn đề này ở rất nhiều project lớn và phát triển lâu đời rồi).

Vậy hậu quả của nó là gì?

  • Rất khó maintain: Fix 1 bug lòi ra 1 đống bugs khác. Đổi một business logic lòi ra một đống bug (impact rất lớn).
  • Khó mở rộng và thêm tính năng mới: Các bạn phải làm việc với những thành phần cũ kĩ của hệ thống. Components thì cũ kĩ, phải biết kết hợp các phần cũ và mới (đòi hỏi phải nhiều skill và kinh nghiệm). Cho nên thường chỉ có các ông senior làm mới ổn thôi. Tay ngang vào chỉ có tạch. Và khi thêm tính năng mới, nhiều project chấp nhận sống chung với lũ, viết code theo lối mòn cũ. Dẫn tới một cục siêu to phức tạp khổng lồ - big ball of mud.

Cho nên khi các bạn làm việc với các project phát triển được tầm chục năm trước thì sẽ có những thành phần sau:

  • Những thành phần rất cũ không ai dám đụng (legacy code) và không có unit test.
  • Những thành phần mới hơn (code đẹp hơn) và có unit test đẹp trai.

Phải chi, nếu ban đầu, những project này hoàn toàn được apply bằng Domain Driven Design thì phải hay không. Nhưng đời đâu như mơ. Cái gì cũng phải có đánh đổi - trade off. Phải release cho sớm, phải kiếm tiền cho nhanh. Có tiền đập đi xây lại sau. Nhưng có tiền đâu mà đòi đập đi xây lại. Trừ khi công ty bạn giàu có như Facebook và Google.

Và nếu sản phẩm được design xịn ngay từ đầu. Thì giai đoạn đầu code sẽ cực và lâu hơn. Nhưng giai đoạn maintain sau này sẽ sung sướng hơn rất nhiều.

Hành trình về Phương Đông, à nhầm về Domain Driven Design

Nó là gì?

Như mình nói ở trên, bạn có thể hiểu nó là một hệ tư tưởng, một cách tiếp cận để phát triển phần mềm.

Và...DDD tập trung vào domain (nghiệp vụ):

  • Tất cả business logic đều tập trung vào core domain của nó. Hiểu đơn giản là có một layer là domain, tất cả logic nghiệp vụ sẽ được viết trong layer này. Khi kết hợp với hexagonal, onion hay clean architecture, nó thường nằm ở layer domain. Tách biệt hoàn toàn so với các layer khác và không phụ thuộc vào bất cứ layer hay công nghệ nào ví dụ database, message queue, UI, API, ... Mà các layer khác phải implement layer domain này. Đây là đảo ngược sự phụ thuộc.
  • Một điều quan trọng của DDD: devs team, khách hàng và các team liên quan sẽ có một ngôn ngữ chung - Ubiquitous Language. (Mình sẽ nói về điều này sau) Để cho giữa các bên (các teams) hiểu nhau nhất. Có một sự thật là có rất nhiều project thất bại vì chưa hiểu rõ yêu cầu của khách hàng. Hoặc làm sai yêu cầu và phải làm lại. Tất cả đều do...chưa hiểu đúng spec hoặc hiểu sai hoặc hiểu thiếu.

Vậy khi nào thì nên áp dụng DDD?

Nếu một project nhỏ, nghiệp vụ ít, thì không cần phải dùng DDD. Nó giống như bạn "dùng súng lục để bắn con muỗi" vậy đó.

Mình nhớ không lầm thì theo cuốn sách "Domain Driven Design in PHP" nói rằng: Một project trên 30 use cases thì mới nên apply DDD.

Mình cũng đồng ý với câu trên nhưng có bổ sung một ít: Khi project có business phức tạp thì mới cần apply DDD. Đặc biệt là các enterprise projects.

Đi sâu vào building blocks và những thứ liên quan

Trước khi vào những khái niệm cốt lõi của building blocks thì mình đi những thứ liên quan trước.

Who is domain expert?

Đơn giản thôi! Là người/nhóm người hiểu rõ về domain nhất. Để hiểu rõ thì sẽ phải trải qua giai đoạn Strategic Design nhoa.

Nếu bạn được yêu cầu làm một software liên quan tới máy bay. Thì hãy ăn ngủ với các anh phi công và các em tiếp viên xinh gái để xã giao với nhau và hiểu về domain đó.

Nếu bạn muốn tìm hiểu về domain shipping thì hãy hỏi các anh shipper (hỏi mình nè).

Nếu các bạn muốn tìm hiểu về domain thương mại điện tử. Thì hỏi vợ bạn kà. Mình đùa chút thôi.

Những người đó có thể là domain expert. Này trong sách nói nha. Chứ theo ý kiến cá nhân của mình. Đại khái là ở một lĩnh vực nghiệp vụ (domain) nào. Thì cũng có một nhóm người cực kì hiểu rõ về lĩnh vực đó. Bạn hãy tìm đến họ. Có thể trong công ty bạn anh CTO, anh leader là domain expert thì sao. Ai mà biết được.

Ubiquitous Language

Mình hiểu nó là ngôn ngữ chung (đã nhắc ở trên). Hay ngôn ngữ thống nhất. Ở đây là thống nhất giữa domain expert team, devs team và các team liên quan.

Khi các team trên nói chuyện với nhau. Mọi ngôn từ, ý nghĩa đều thống nhất, đều được hiểu như nhau. Và vì điều này, giữa các team, ví dụ khách hàng và dev team sẽ không hiểu lầm nhau. Dẫn tới ít khi bị mismatch requirement, tránh sai sót. Từ đó khả năng thành công của project càng cao hơn.

Chứ nếu business mà khách hàng nói là "khi đơn hàng mới được tạo thì sẽ có trạng thái là pending và sau đó nếu thanh toán thất bại thì order sẽ có trạng thái là canceled". Mà các bạn (devs team) lại giao tiếp lại: à mình sẽ tạo một method createOrder với param là Orders được submit từ endpoint abcxyz và sau đó validate order rồi persist xuống DB, response ra orderTrackingId để có thể follow được status của order. Sau đó sẽ dùng SAGA để quản lý transaction trong microservices, và khi payment thất bại sẽ bắn event lên message queue để SAGA sẽ process rollback order.

Và khách hàng hỏi, bạn release tính năng tạo đơn hàng chưa? Devs team sẽ hỏi nhau SAGA flow làm xong chưa? Handle exception tới đâu rồi, class OrderSaga xử lý chưa đúng, ....

Nghe hơi mệt. Nó chưa có sự đồng nhất về mặt ngôn ngữ.

Từ từ sẽ dẫn tới mismatch requirement và sẽ kéo theo các vấn đề khác.

Cho nên Ubiquitous Language là một khái niệm quan trọng trong DDD. Từ những ngôn từ chung này, chúng ta đưa chúng vào trong code. Ví dụ đặt tên method, tên biến, tên class, các action, database... đều đồng nhất với ngôn ngữ chung. Và khi các team nói chuyện với nhau. Đều sử dụng chung một loại ngôn ngữ, 1 hệ thống ngôn từ và hiểu ý nhau. Điều này là quá tuyệt vời. Nhưng trên thực tế không phải team nào, công ty nào cũng làm được điều này đâu nếu số lượng nhân sự càng lớn (mình đã chứng kiến điều này trong cty mình). Sẽ càng khó khăn hơn nếu chúng ta lại có rào cản ngôn ngữ như khách hàng người Nhật, devs người Việt, ....

Bounded Context

Trong software của chúng ta, chúng ta sẽ phải phân chia các domain logics, những Ubiquitous Language thành các context nhỏ hơn.

Mình lấy ví dụ trong trang báo điện tử của mình:

  • Mình có user bounded context: Nơi chứa logic nghiệp vụ liên quan tới users, các từ ngữ liên quan tới users, roles.
  • Mình có post bounded context: Chứa logic nghiệp vụ liên quan tới các bài post, ...
  • Và khi mình nói, số lượng user xem bài post abc này trong một tháng 100 users. Thì ý nghĩa của user trong post context sẽ khác với user trong user context. Rõ ràng user trong user context thì nó đang đề cập tới, user admin, author, subscriber, hay có role là gì, ... Còn user trong post context đơn giản là user đã xem bài post ở ngoài thôi.

Đó chính là bounded context!

Bounded context example

Layer architecture

Lịch sử một chút!

Thời sơ khai của software chúng ta hay thấy là họ viết 1 đống code vào 1 hoặc 1 vài file (1 file làm tất cả từ controller, business logic, persist data, view, ...). Và dần dần ông cha ta thấy có quá nhiều vấn đề nên đã phát minh ra layer architecture bằng chia ra thành nhiều layer nhỏ hơn. Mỗi layer làm một việc duy nhất như: UI layer, application layer, domain layer, infrastructure layer, ...

Thời gian trôi qua thì các architecture như hexagonal, onion hay clean architecture lại ra đời với mục đích đơn giả và duy nhất là giúp software của chúng ta dễ maintain, mở rộng và scale.

Và DDD tập trung hết core business logic vào một "nơi" duy nhất như bạn đã đọc ở trên. Nên thông thường DDD sẽ thường được kết hợp với các layer architecture để triển khai một cách dễ dàng nhất.

Đó là lý do tại sao bạn hay thấy, khi code với DDD, người ta hay đi kèm hexagonal hay clean architecture.

Event sourcing

Hẹn các bạn trong một bài viết khác mình sẽ đi sâu về event sourcing.

Modeling skill

Theo mình đây là một số skill khó.

Và mình đang nói về lập trình hướng đối tượng (OOP). Trong thế giới software, chúng ta phải làm việc với các đối tượng (object). Nên skill modeling (1 phần trong tactical design) là yêu cầu bắt buộc khi làm việc với DDD.

Làm sao để mô hình hóa thành các object tối ưu nhất? Quan hệ giữa các object như thế nào (object relationship)? Nói cụ thể hơn là thiết kế các DTO, value object, entity, đặt biệt là aggregate (nói ở dưới nhé). Hay thiết kế các model mapping database, schema, .... Tất cả những thứ này là modeling. Bạn phải có kinh nghiệm làm việc với nó...bằng cách thực hiện nhiều thôi. Hoặc đọc tiếp bài viết.

Trong DDD, khi bạn modeling data thành các object. Nó sẽ sinh ra một vấn đề (người khác hay nói): có quá nhiều object được sinh ra ở các layer, dẫn tới sự trùng lặp về mặt code, nên rối. Nhưng đối với cá nhân mình, nó không phải là vấn đề. Vì mình đọc code theo flow request (đi từ controller vào các layer trong), thì cần đọc object nào thì đọc thôi. Đâu cần xem hết cả đống object làm gì. Cái quan trọng là business logic chứ không phải số lượng object model sinh ra.

Mình thấy nhiều bạn ít có thói quen modeling. Các bạn thích dùng array và các kiểu dữ liệu nguyên thủy (primitive type) như integer, string, ... . Bạn nào có kinh nghiệm nhiều sẽ thấy! Ví dụ dùng array, bạn sẽ không biết array đó đang chứa gì, 1 đống dữ liệu trong đó. Đặc biệt là các bạn JS, PHP developers.

Và một điều quan trọng tiếp theo! Trong DDD, modeling phải tuân thủ ngôn ngữ chung của domain. Đây dường như là điều bắt buộc. Bạn có thể quay lại đọc phần ngôn ngữ chung ha.

Data transfer object (DTO)

Không biết các bạn định nghĩa DTO như thế nào. Đối với mình đơn giản lắm. Nó là một object dùng để chuyển data đi qua các layer trong vòng đời của một business flow.

Ví dụ mình có một UserDto dùng để chuyển dữ liệu từ controller sang các layer bên trong:

Copy
// UserDto.java
// Object đơn giản thôi chứ thực tế nhiều fields hơn nha.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UserDto {
    private String name;
    private String email;
    private String password;
}

Một DTO thì sẽ các các method getter, setter. Và có thể có một số method khác để có thể tạo dữ liệu một cách dễ dàng hơn như fromArray, ....

Từ nảy tới giờ mình đã giới thiệu cho các bạn khá nhiều các khái niệm trong DDD. Tiếp theo chúng ta sẽ đi đến những thứ cốt lõi của DDD.

Value Object

Đây là một thành phần quan trọng trong DDD. Và ngắn gọn như sau:

  • Value object là một object, và dùng để chứa dữ liệu.
  • Immutable - bất biến. Nghĩa là khi đã khởi tạo value object thì không thể thay đổi data bên trong nó. Đây là tính chất cực kì quan trọng giúp cho dữ liệu được toàn vẹn - không bị thay đổi trong vòng đời 1 business flow.
  • Vì nó là immutable nên sẽ không có các public setter và các properties là read only.
  • Hai value object có dữ liệu giống nhau thì được xem là bằng nhau.
  • Những business logic liên quan sẽ được đặt bên trong các value object.

Mình lấy ví dụ luôn.

Copy
// UserName.java
@Getter
public class UserName {
    private static final int MIN_LENGTH = 3;
    private static final int MAX_LENGTH = 50;

    private String value;

    public UserName(String value) {
        setValue(value);
    }

    private void setValue(String value) {
        if (value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
            throw new InvalidUserNameException();
        }

        // Ngoài ra còn các business logic khác như:
        // username không được chứa các ký tự đặc biệt
        // ...

        this.value = value;
    }
}

Khi bạn muốn xài value object UserName.java`

Copy
// Khởi tạo một value object
UserName userName = new UserName("lenhatthanh20");

// Bạn không thể thay đổi data bên trong nó nữa
userName.setValue("admin"); // Điều này không cho phép

Và có một điều cực kì quan trọng mình muốn nhấn mạnh:
Khi một value object được tạo thành công. Tất cả business logic liên quan tới object đó đã được thỏa mãn. Và dữ liệu trong value object không được thay đổi nữa --> Data consistency.

Khi bạn muốn vi phạm một business logic nào đó. Trong ví dụ UserName là cố gắng tạo một user name có chiều dài nhỏ hơn 3, thì chương trình sẽ throw Error ngay. Ví dụ:

Copy
// Khi bạn cố gắng làm điều này, thì chương trình sẽ throw error
UserName userName = new UserName("le");

Bạn đã thấy sự lợi hại của việc đặt business logic bên trong các value object chưa.

Entity

Nếu các bạn hay dùng các backend framework thì chắc cũng đã nghe nhiều. Mình tóm tắt một xíu thôi.

  • Cũng là một object giống y value object.
  • Điều khác biệt là, entity có định danh (ID).
  • Và dĩ nhiên 2 entity có ID khác nhau thì chúng ta xem 2 entity đó khác nhau
  • Có setter. (Nhưng theo mình nên hạn chế dùng setter thôi, properties nào cần thì mới dùng)
  • Thường được dùng để chứa dữ liệu và lưu xuống DB.
  • Và những business logic liên quan sẽ được đặt bên trong các entity.

Ví dụ một chút:

Copy
// Mình có một file Entity.java để dùng chung cho tất cả các entity
// Base class Entity.java
@Getter
@AllArgsConstructor
public class Entity<Type> {
    private Type id;
}

// Và đây là một entity trong DDD
// Role.java 
@Getter
public class Role extends Entity<Id> {
    private RoleName name;
    private RoleDescription description;

    public Role(
            Id id, 
            RoleName name, 
            RoleDescription description
    ) {
        super(id);
        this.name = name;
        this.description = description;
    }

    public void updateRoleName(RoleName name) {
        // Một số business logic có thể nằm ở đây
        this.name = name;
    }

    public void updateRoleDescription(RoleDescription name) {
        // Một số business logic có thể nằm ở đây
        this.description = description;
    }

    public static Role create(
            Id id, 
            RoleName name, 
            RoleDescription description
    ) {
        // Một số business logic có thể nằm ở đây
        Role role = new Role(id, name, description);

        return role;
    }
}

Và cũng giống với value object. Khi bạn tạo thành công một entity, tất cả business logic có liên quan sẽ được thỏa mãn --> Data consistency.

Và bạn thử nhìn Role entity xem. Rõ ràng business của role là, có thể tạo mới một role, có thể update role name, có thể update role description (đây là ngôn ngữ chung). Và chúng ta thể hiện điều này trong Role entity luôn.

Repository

Đây là một pattern để giao tiếp với database. Chắc mình không nói thêm về repository nữa. Chắc hầu hết các bạn đều biết rồi. (Còn nếu chưa biết thì có thể xem google nhé).

Domain service

Mình nhắc lại một lần nữa! Các business logic sẽ tập trung vào domain layer. Cụ thể là value object, entity, aggregate (tí tìm hiểu cái này) và domain service.

Trong layer architecture mình giới thiệu lúc đầu. Thì thông thường mỗi layer sẽ có các service của layer đó. Domain service chứa các logic để phục vụ cho layer domain. Logic ở đây chính là business logic.

Vậy khi nào thì domain service xuất hiện?

Theo kinh nghiệm của mình (một tip nhỏ)! Khi các business logic bạn không biết đặt nó ở đâu. Nghĩa là đặt trong value object, entity, hay aggregate đều không được. Thì lúc đó, hãy nghĩ đến domain service.

Mình lấy ví dụ trong use case tạo một role có 1 business logic sau:

  • Khi tạo mới một role, tên của role mới này bắt buộc không được trùng tên với bất kì role nào trong hệ thống.

Và mình cũng không đặt logic trên vào các objects kia được. (Bạn mà đặt được thì hú mình ở comment nha). Cho nên mình sẽ tạo một domain service như sau:

Copy
// CreateRoleService.java
@Service
@AllArgsConstructor
public class CreateRoleService implements CreateRoleServiceInterface{
    RoleRepositoryInterface roleRepository;

    public void create(RoleDto roleDto) {
        this.roleNameDoesNotExistOrError(roleDto.getName());

        Role role = Role.create(
                new Id(UniqueIdGenerator.create()),
                new RoleName(roleDto.getName()),
                new RoleDescription(roleDto.getDescription())
        );

        roleRepository.save(role);
    }

    // Đây là business logic mình vừa đề cập
    // Và mình đặt logic này trong domain service.
    private void roleNameDoesNotExistOrError(String name) {
        Optional<Role> role = roleRepository.findByName(name);
        if (role.isPresent()) {
            throw new RoleAlreadyExistException();
        }
    }
}

Thật ra cũng có nhiều tranh cãi về Domain service hay có nhiều ý kiến khác nhau. Người thì cho rằng những logic như trên nên nằm ở ngoài Application Service thay vì domain service. Ví dụ trong Clean Architecture, thì có vẻ những logic như vậy sẽ nằm ở layer use case. Mà use case thì vẫn có người cho rằng nó tương đương với application service trong DDD. Nhưng vẫn có người cho rằng nó thuộc với layer domain của DDD. Việc này theo mình cũng không có vấn đề gì lớn. Vì DDD chỉ là cách tiếp cận, chắc chắn sẽ có nhiều cách implement. Miễn sao nó phù hợp với bạn, với team và doanh nghiệp của bạn là được.

Tiếp theo, chúng ta đến phần khó nhất và và hay nhất trong DDD.

Aggregate

Trước khi đi vào aggregate mình sẽ đi use case: Tạo comment của một bài báo (post).

Và mình sẽ có một số business logic như sau:

  • Khi tạo comment, nếu user không tồn tại thì sẽ báo lỗi cho người dùng.
  • Khi xóa bài báo thì tất cả comment sẽ bị xóa theo.
  • Tổng số lượng comment trong 1 bài báo là 100 (lưu ý đây chỉ là logic ví dụ - không phải logic trong production application). Nếu quá 100 thì sẽ báo lỗi cho người dùng.

Những business logic khác như về content, slug, summary, ... hay về quyền (permission) thì mình không đề cập ở đây.

Lưu ý: Các logic ở trên chỉ là ví dụ để giúp mình giải thích về Aggregate. Chứ thực tế cũng ít người làm vậy. Nhất là 2 logic cuối ha.

Bây giờ là cách mà các bạn hay code nè!

Copy
// CHƯA ÁP DỤNG DDD
// Ở TRONG MỘT FILE SERVICE NÀO ĐÓ CỦA BẠN
// commentDto: userId, postId, content

// Dưới đây sẽ là các step các bạn hay làm:

// STEP 1. Kiểm tra xem user có tại hay không.
this.checkingUserExistOrError(userId);

// Còn nhiều business logic khác ở đây: quyền, ...

// STEP 2. Kiểm tra POST có tồn tại hay không.
Post post = getPostOrError(postId);

// STEP 3. Kiểm tra số lượng comments của POST
// Hoặc ở đây bạn hay đếm số lượng comments từ database
if (post.getComments().size() >= 100) {
    throw new PostCommentLimitException();
}
// Check một số business logic khác nữa.

// STEP 4. Save comment xuống:
Comment comment = new CommentEntity(...);
commentRepository.save(comment); // Lưu xuống DB

Bạn hãy nhìn những step ở trên. Chúng ta thông thường sẽ có khá nhiều business logic liên quan tới comment ở bước số 3.

VÌ MỘT LÝ DO NÀO ĐÓ ở trong một use case add comment khác use case ở trên (ví dụ trong tính năng khác, màn hình khác, hoặc trong API khác như quick add, add multiple, ...). Các anh dev cũng thao tác với Post model và quên logic ở step 3 (hoặc làm sai), và trực tiếp add comment ở step 4. Sau đó lưu lại ở step 5. Dẫn tới có thể gặp trường hợp add được 101 comment. Thì data vẫn lưu lại thành công nhưng vi phạm business logic số lượng comment không được quá 100. (Và khi các anh dev copy logic thì code sẽ bị duplicate nữa).
Lúc này data ở trong Post model không nhất quán (inconsistency).

  • Aggregate giúp cho data bên trong nó nhất quán ở đây có nghĩ là. Khi một aggregate tạo thành công, thì sẽ thỏa mãn tất cả business logic liên quan tới nó. Nó chính là consistency. (Nghĩa là khi Post aggregate được tạo thành công, chắc chắn số lượng comment trong post phải nhỏ hơn hoặc bằng 100).
  • Chứ không phải lúc thì consistency, lúc ông dev copy thiếu thì inconsistency. Vậy là toang!

Đó là lý do Aggregate ra đời để giải quyết bài toán inconsistency data trong business flow của application. Và chúng ta phải design aggregate cẩn thận và hợp lý nhất. Đây là cũng 1 trong những phần khó nhất.

Quay lại, khi add comment vào aggregate. Làm sao chúng ta có thể chắc chắn rằng business logic luôn được thỏa mãn? Câu trả lời là hãy dùng aggregate và đặt business logic đó ngay bên trong aggregate.

Đặt như sau:

Copy
// Post.java
// `Post` chính là aggregate root.
// Để thao tác với các thành phần bên trong như entity `Comment`
// thì tất cả phải thông qua aggregate root.
// Ví dụ thao tác `add comment`
public class Post extends AggregateRoot<Id> {
    
    // Đây là property `comments` để chứa comment trong aggregate.
    // Nó thể hiện mối quan hệ - object relationship.
    // Mỗi post sẽ có nhiều comments ở bên trong nó.
    // Và `Comment` chính là một entity
    private List<Comment> comments = new ArrayList<>();

    // Method này nằm bên trong aggregate root
    public void addComment(String content, String userId) {
        // True invariants here (đây là logic buộc phải thõa mãn)
        // Tất cả comment phải nhỏ hơn hoặc bằng 100.
        if (this.comments.size() > MAX_COMMENT) {
            throw new CommentLimitExceededException();
        }
   
        // Ví dụ còn một số business logic khác phải thỏa như:
        // Khi bài post ở trạng thái DRAFT, không thể add comment
        // Ví dụ:
        if (this.status != 'DRAFT') {
            throw new CommentPermissionException();
        }
        
        // ... và nhiều logic ở đây nữa

        Comment comment = new Comment(
            newId(UniqueIdGenerator.create()),
            content, 
            new Id(userId)
        );
        this.comments.add(comment);
}

// Domain service AddCommentService.java (hoặc application service)
// Và ở ngoài domain service chúng ta sẽ làm như sau:
// 1. Kiểm tra xem user có tại hay không.
this.checkingUserExistOrError(userId);

// 2. Kiểm tra post có tồn tại hay không.
Post post = getPostOrError(postId); // Load aggregate

// 3. Thêm comment vào post:
post.addComment(
    commentDto.getContent(), 
    user.get().getId().toString()
);

// Sau khi trãi qua step 3, toàn bộ business logic sẽ được thỏa mãn.

// 4. Lưu post:
postRepository.save(post); // Lưu xuống DB

Chỉ cần chốt lại 1 điều. Khi addComment được thực hiện thành công! Tất cả business logic sẽ được thỏa mãn. Nghĩa là data trong aggregate được toàn vẹn và nhất quán (consistency). Không có chuyện có thể add được 101 comments.

Code của chúng ta viết rất tuân thủ theo ngôn ngữ chung. Nói sao làm vậy - add comment vào post, post.addComment(...).

Note: Ngoài ra còn 1 option nữa để addComment, bạn có thể để nó trong một domain service. Và service này có thể được xài ở nhiều nơi, mỗi lần muốn add comment thì xài service này, việc này vẫn đảm bảo data consistency. Tùy ý bạn thôi, DDD thì linh hoạt, sẽ có nhiều cách implement. Hãy chọn cách phù hợp với mình. Còn mình thì thích để trong aggregate root hơn và đặt luôn business logic vào đó.

Mình nhắc lại đây là phần khó nhất trong DDD.

Và để hiểu rõ thêm về ví dụ trên thì dưới đây là tính chất và rule của Aggregate:

  • Aggregate là tập hợp nhiều entity, value object có liên quan tới nhau.
  • Trong aggregate sẽ có một aggregate root. (Aggregate root cũng là một entity).
  • Aggregate sẽ có một ID (gọi là global ID).
  • Các aggregate giao tiếp với bên ngoài chỉ thông qua global ID.
  • Các object bên trong aggregate tuyệt được không được giao tiếp với bên ngoài. Tất cả phải thông qua aggregate root.
  • Dữ liệu bên trong aggregate sẽ được toàn vẹn và nhất quán (consistency). Đã giải thích ở trên một chút và tí nữa giải thích thêm vì nó rất quan trọng.
  • KHI SAVE AGGREGATE PHẢI THEO CƠ CHẾ ATOMIC: Nghĩa là tất cả thông tin trong aggregate phải được save xuống thành công tất cả hoặc tất cả thất bại. Và khi có các request đồng thời, phải xử lý cho chuẩn - concurrency requests. Chổ này liên quan tới xử lý concurrency requests theo các cơ chế như optimistic locks hay pessimist locktransaction. Mình xin phép nói ở một bài khác.
  • Khi làm việc với aggregate, đừng nghĩ tới database relationship. Mà hãy nghĩ tới object relationship.

Bây giờ sẽ là design chi tiết hơn:

Mình design aggregate Post như hình vẽ bên dưới:

Post aggregate example

Note: Nhìn hình ta thấy, một aggregate là tập hợp nhiều objects (nhiều files) lại với nhau ha.

Thật ra có nhiều value object hay entity khác trong Post aggregate nữa. Ví dụ category, tag, featured image, meta data .... Nhưng mình không có vẽ vào vì sẽ rối, mình chỉ vẽ tượng trưng một vài thôi.

À để ý một chút, Comment ở trên chính là một entity và nó nằm bên trong Post aggregate nha. Các bạn cũng đã học về Entity ở trước đó rồi. Thì Comment cũng có 1 ID, nhưng nó không phải là global ID. Và bên ngoài aggregate không thể thao tác với ID này. Mọi thao tác đều phải thông qua aggregate root - Post.

Mình hoàn thiện code Aggregate root luôn. Các entity, value object khác mình code nha vì nó giống Roleentity và UserName value object ở phần trên:

Copy
// Post.java
// Đây là Aggregate root
@Getter
@Setter
public class Post extends AggregateRoot<Id> {
    private Title title;
    private PostContent content;
    private Id userId;
    private Summary summary;
    private Slug slug;
    private List<Comment> comments = new ArrayList<>();
    // Thật ra còn nhiều properties khác ở đây nữa

    public Post(
            Id id, 
            Title title, 
            PostContent content, 
            Id userId, 
            Summary summary, 
            Slug slug
    ) {
        super(id);
        this.setTitle(title);
        this.setContent(content);
        this.userId = userId;
        this.summary = summary;
        this.setSlug(slug);
    }

    public void updateTitle(Title title) {
        this.title = title;
    }

    public void updateContent(PostContent content) {
        this.content = content;
    }

    private void updateSlug(Slug slug) {
        this.slug = slug;
    }

    public static Post create(
            Id id, 
            Title title, 
            PostContent content, 
            Id userId, 
            Summary summary, 
            Slug slug
        ) {
        return new Post(
            id, title, content, userId, summary, thumbnail, slug
        );
    }

    public void addComment(String content, String userId) {
        // True invariants here, example
        // Total of comments must be less than 100
        // When the status of the post is DRAFT, can not add comment
        if (this.comments.size() > MAX_COMMENT) {
            throw new CommentsLimitExceededException();
        }
       
        // ...

        Comment comment = new Comment(
            new Id(UniqueIdGenerator.create()),
            content, 
            new Id(userId)
        );
        this.comments.add(comment);
    }
}

Và kết lại được một số Rule design aggregate như sau!

  • Rule 1: Dựa trên các business logic luôn đúng - True Invariants
  • Rule 2: Aggregate nên nhỏ nhất có thể.
  • Rule 3: Giao tiếp với Aggregate khác bằng global ID
  • Rule 4: Nên sử dụng Eventual consistency

Mình sẽ đi giải thích từng cái ha. Chắc giải thích gọn gọn xíu thôi!

Rule 1: Như ví dụ ở trên, số lượng comment không quá 100 trong một bài viết chính là một true invariants. Vì như mình nói. Khi aggregate được tạo ra (hoặc update như add comment) thì mọi business logic phải được thỏa mãn và luôn đúng. Cho nên khi thiết kế aggregate, bạn phải đặt các business logic cho thích hợp.

Rule 2: Aggregate nên nhỏ nhất có thể. Ở trên ví dụ hồi nãy, thật ra mình không nên đặt Comment entity vào trong Post aggregate nếu không có logic số lượng comment không quá 100 trong một bài viết. Tại sao lại như vậy? Lý do là ở step 2, mình có load toàn bộ aggregate lên (cùng với comments): Post post = getPostOrError(postId);. Cho nên sẽ có vấn đề liên quan tới performance nếu xử khi không khéo (không lazy load, ...). Khi có nhiều comment (ví dụ 1 triệu comment) thì việc load lên như thế sẽ nặng. Đó là lý do mình giới hạn số lượng comment.

Cho nên khi một aggregate quá lớn sẽ dẫn tới vấn đề performance. Và logic quá nhiều sẽ dẫn tới phức tạp. Đủ dùng thôi, cái gì lớn quá cũng không tốt phải không nào =]].

Lưu ý là nếu business của bạn không giới hạn comment, thì hãy tách Comment ra thành một aggregate khác nhé. Và khi xóa bài viết, muốn xóa comment ta có thể sử dụng cơ chế động bộ (sync) hoặc bất đồng bộ (hay còn gọi là Eventual consistency). Dùng cái nào cũng có đánh đổi (trade-off) cả. Mình sẽ nói sau vấn đề này.

Rule 3: Bạn cũng đã thấy rõ qua các ví dụ rồi. Các aggregate thường giao tiếp với nhau thông qua global ID. Như bạn thấy trong Aggregate root Post, mình giao tiếp với User aggregate thông qua ID thôi.

Các aggregate giao tiếp thông qua global ID

Rule 4: Để nói sau ở phần Domain event nha!

Túm cái váy lại về aggregate

  • OOP (lập trình hướng đối tượng). Khi làm việc với DDD hay aggregate. Chúng ta chỉ nói chuyện liên quan tới object, object relationship hay tổng quan hơn chính là OOP. Và chúng ta chỉ nói chuyện liên quan tới domain, nghiệp vụ. Chứ không phải thuần về technical như framework, language, 1-n, n-1, ....
  • Nhất quán, toàn vẹn dữ liệu. Khi tạo/update/delete aggregate, tất cả business logic sẽ được thỏa mãn. Dữ liệu được toàn vẹn và nhất quán. Xóa bài post thì tất cả comment bị xóa đi. Add comment thì tối đa 100 không bao giờ lên 101. Chứ không có chuyện nữa nạc nữa mỡ. Nhưng, mình xin nhắc lại là vẫn còn một phần cực kì quan trọng là aggregate phải được save theo cơ chế atomic nha.
  • Kết hợp với nhiều "công cụ" khác để tăng performance như domain event.

Domain event

Nói một cách ngắn gọn thì nó là một event (sự kiện). Nó xảy ra khi có một sự kiện nào đó trong domain layer. Ví dụ cho dễ hiểu ha.

Khi tạo một user. Trong domain layer sẽ xảy ra sự kiện aggregate tạo thành công aggregate. Thì ngay lúc đó sẽ có một event tên là UserCreatedEvent được bắn ra. Bắn ra thôi còn ai hứng event đó xử lý thì kệ nó.

Bây giờ thì ai sẽ hứng event ha? Một aggregate khác (một context khác) sẽ hứng event này.

Cụ thể một chút! Ví dụ như mình xóa một bài post, sẽ có event được bắn ra PostDeletedEvent. Sau đó Comment aggregate sẽ hứng event này và sẽ xóa đi các comment liên quan. Và nếu ở đây chúng ta xử lý bất đồng bộ (async) ví dụ sử dụng một message queue như Kafka. Thì đây chính là khái niệm Eventual consistency như mình đề cập ở Rule 4 trên phần aggregate. Nghĩa là, khi xóa bài post, thì request delete sẽ response ngay khi xóa thành công (lúc này chưa xóa comments đâu nha). Một lúc sau đó (thường cực kì nhanh) thì comments mới được xóa thông qua domain event và message queue (async). Mình hay gọi là một lúc nữa thì data sẽ nhất quán. Đó cũng là một sự đánh đổi khi dùng eventual consistency.

Hoặc trong kiến trúc microservices. Khi user tạo một đơn hàng thành công. Order service sẽ bắn ra event OrderCreatedEvent. Sau đó payment service (và các service khác) sẽ "chụp" event này để tiếp tục xử lý.

Khi chúng ta giao tiếp qua event như thế (event driven architecture). Thì sẽ giúp tăng tính performance và scalability. (bạn có thể search Google về ý này nha). Nhưng đánh đánh đổi với nó là có nhiều thứ phức tạp hơn như vấn đề xử lý transaction khi có error xảy ra hay debug, .... Những thứ transaction này liên quan tới các pattern như SAGA, Transaction outbox. Mình sẽ trình bày chi tiết ở một bài viết khác nha. Lại bài khác. Thật ra tới level này thì nó còn vô vàng các thứ xung quanh. Các bạn thông cảm =]].

Và cuối cùng, domain event thường được đăng ký bên trong aggregate root (Chổ này tùy thôi, có thể đăng ký trong trong domain service cũng được, bạn thiết kế sao cho hợp lý là được). Hoặc bạn có best prac·tice nào về domain event thì góp ý cho mình nha. Còn mình cũng thấy làm vậy hợp lý.

Ví dụ một chút!

Copy
// Hàm này bên trong Post.java (aggregate root)
public static Post create(
        Id id, 
        Title title, 
        PostContent content, 
        Id userId, 
        Summary summary, 
        String thumbnail, 
        Slug slug
) {
    // True invariants here
    Post post = new Post(
        id, 
        title, 
        content, 
        userId, 
        summary, 
        thumbnail, 
        slug
    );
    
    // Khi tạo post thì sẽ đăng ký một event
    post.registerEvent(new PostCreatedEvent(post));

    return post;
}

// Và ở đâu đó trong layer repository
// Khi persist DB xong event sẽ được tự động bắn đi
// Hoặc bạn có thể bắn event trong Domain service (tùy bạn)
// Nâng cao hơn thì việc commit DB 
//     + publish event nó còn dính tới transaction. 
// Bạn tự tìm hiểu thêm nha
// Mình ví dụ nếu save DB không thành công thì không được bắn event đi nha.
// Khi bắn event đi thất bại thì phải xử lý như thế nào?
// vân vân mây mây
public class PostRepository implements PostRepositoryInterface {
    private PostJpaRepository postJpaRepository;
    ...

    @Override
    public void save(Post post) {
        // Build PostEntity here
    
        this.postJpaRepository.save(postEntity);
        // Publish domain events
        user.publishEvents(domainEventPublisher);
    }
}

// Và đây là nơi nhận được event.
// Mình bắn event vào Kafka để sync data qua redis
// Ở đây sẽ là ngoài phạm vi của domain layer nha.
// Domain layer chỉ có nhiệm vụ bắn domain event đi thôi.
@Service
@AllArgsConstructor
public class PostEventHandler {
    @EventListener(PostCreatedEvent.class)
    public void handlePostCreatedEvent(PostCreatedEvent event) {
        sendMessageToKafkaBroker(event);
    }

    private void sendMessageToKafkaBroker(DomainEventInterface event) {
        Post post = (Post) event.getEventData();
        ProducerRecord<String, PostEventDto> record 
            = new ProducerRecord...
        
        // send data to kafka
        this.kafkaTemplate.send(record);
    }
}

Tới đây chắc bạn cũng đã hiểu phần nào về domain event rồi.

Kết luận một chút về domain layer

Mình có rút lại một chút kết luận:

  • Khi làm việc với DDD thông thường sẽ kết hợp với các layer architected.
  • Business logic sẽ tập trung ở duy nhất một nơi gọi là core domain layer.
  • Bạn có thể viết unit test riêng biệt cho domain layer luôn để verify các business logic vì domain layer không phụ thuộc vào các layer khác.
  • Và các layer khác phải dựa trên domain layer để mà implement. Ví dụ trong hexagonal architecture, bạn sẽ thiết kế các input ports và output ports. Cụ thể nó có thể là các interface của domain layer. Và các layer khác muốn giao tiếp với domain layers sẽ phải implement các interfaces này. Đây là đảo ngược sự phụ thuộc (Inversion of control).

Vẫn là câu hỏi: Tại sao phải dùng DDD?

Nếu business logic trong ứng dụng của bạn ít và không phức tạp (ví dụ thuần CRUD) thì bạn có thể bỏ qua DDD.

Đối với các ứng dụng enterprise với business logic phức tạp:

  • Hầu hết frameworks đều dùng layer architecture style.
  • DDD dùng tốt với layer architecture, ngoài ra còn có những thứ mạnh mẽ như aggregate, domain event.
  • DDD tập trung business logic lại ở layer domain. Giúp việc thay đổi spec (business logic) dễ dàng hơn. Maintain, scale dễ hơn. Kiếm tiền ngon hơn về lâu về dài.

Và những lợi ích của DDD mình đã đề cập trong suốt bài viết rồi. Vậy thì tại sao chúng ta không sử dụng DDD!

Khó khăn khi dùng DDD

Phức tạp hóa vấn đề nếu làm không đúng hoặc không đến nơi đến chốn.

Đòi hỏi toàn bộ dev team phải nắm được DDD. Mình đã từng gặp vấn đề này rồi. Khi trong dev team các bạn có nhiều bạn chưa nắm được DDD thì sẽ gặp khá nhiều vấn đề lúc implement. Ví dụ tốn cost review, refactor. Đặc biệt là business logic dễ bị phân tán ra các tầng như application, infra, ... do implement sai rule.

Quá trình design (strategic design, tactical design) thực sự không hề đơn giản. Sau khi trải qua các bước design phức tạp thì mới có thể cho được một bản thiết kế vĩ đại. Như bạn thấy đó, trong ví dụ của mình Comment entity sẽ nằm bên trong aggregate Post. Nhưng trong nhiều trường hợp khác, Comment là một aggregate riêng biệt. Để thiết kế aggregate có khá là nhiều rule. Cho nên chốt lại là phần thiết kế này...hoàn toàn không dễ. Hay nói đơn giản hơn là nó khó và phức tạp. Nhưng cũng may mắn là trên thế giới nhiều ông lớn đã làm qua rồi. Team bạn có thể tham khác từ những người đi trước. Hoặc đọc sách, ngâm cứu rồi làm.

Thực tế, anh em mình từng gặp khá nhiều vấn đề khi design aggregate, hay entity hay value object. Kết quả là phải refactor khá nhiều lần thì mới cho ra được design chuẩn chỉnh.

DDD với microservices

Mình thấy rằng (và nhiều người cũng thấy), DDD nổi như cồn lên sau khi microservices ra đời và phổ biến. Chứ DDD ra đời lâu lắm rồi từ đầu những năm 2000. Nhưng những năm gần đây mới nổi lên lại. Và phải nói 2 ông này kết hợp với nhau nó kiểu như cặp bài trùng vậy.

Có thể bạn đã biết! Các bounded context trong DDD thông thường sẽ tách ra thành các service riêng lẽ. Ví dụ ta có user service, post service, comment service, ....Và giao tiếp với nhau thông qua rest API (sync) hoặc message (async - event driven architecture). Nếu giao tiếp qua message, các aggregate bắn domain event lên các message broker như Kafka, RabbitMQ, .... Kết hợp thêm với các pattern để handle transaction (như SAGA), .... Từ đó cho ra một sản phẩm quá tuyệt vời kết hợp DDD với microservices. Dễ maintain, dễ scale... Và đây là kiến trúc mình thấy các công ty to đang áp dụng rất nhiều.

Và đương nhiên, lại hẹn gặp các bạn ở những kì sau =]].

Lời khuyên cá nhân của mình

Nếu công ty bạn muốn apply DDD, mình nghĩ nó khá là khó khăn để implement DDD một cách trọn vẹn nhất. Bạn đang đứng dưới vai trò là là một người muốn apply DDD. Thì bạn phải tìm cách thuyết phục và training cho tất cả các members trong team/công ty để hiểu hết về DDD thì mới apply được.

Cho nên lời khuyên của mình, đôi khi không cần phải sử dụng tất cả tuyệt chiêu trong DDD đâu. Bạn có thể lấy ra một vài chiêu thức để tu luyện và áp dụng: Ví dụ aggregate, domain event, bounded context. Hoặc có một phiên bản DDD customize cho chính công ty bạn. Miễn là, sau khi release sản phẩm, việc maintain, scale trở nên dễ dàng hơn. Vì thời gian cost bỏ ra maintain sản phẩm sau release mới thật sự nhiều, chứ không phải cứ implement ra sản phẩm là thôi (nhiều dev mới có tư duy sai lầm này - release xong thôi, kệ).

Ok vậy là xong! À chưa xong.

Mình cũng không phải master DDD gì đâu, trên đây chỉ là những trải nghiệm của mình trong quá trình sử dụng DDD cũng như apply vào project doanh nghiệp. Các bạn, các anh chị master DDD có thể để lại comment góp ý nếu cảm thấy sai sót hoặc thiếu sót gì đó, vì mình mong muốn bài viết này hoàn thiện nhất (chắc sẽ còn chỉnh sửa dài dài cho hoàn thiện hơn).

Cám ơn các bạn rất nhiều vì đã dành quá nhiều thời gian để đọc tới đây. Hẹn gặp các bạn trong các bài viết sau cũng liên quan tới DDD.

Các bài viết cùng chủ đề