Xàm xí về Domain Driven Design - Part 4: Strategic Design - Context Maps

- 14 mins

Bài trước chúng ta đã thảo luận về một số khái niệm về Domain, SubDomain, BoundedContext….và phần nào hiểu được cách xác định chúng trong dự án của mình. Chúng ta đã biết Domain của chúng ta là gì, cần phãi giải quyết các SubDomain nào, ứng với các SubDomain chúng ta sẻ đặt vào từng Bounded Contexts nào. Ok, ngang đây với một cái nhìn đơn giản là các Sub Domain, các Bounded Context tách biệt hoàn toàn thì không có vấn đề gì lớn lao, tuy nhiên trong các dự án thực tế nó không đơn giản như vậy. Các chức năng luôn có xu hướng trãi dài qua nhiều Entities nằm trong nhiều Bounded Context/Sub Domain khác nhau. Do đó để tạo ra một kiến trúc tốt bắt buộc chúng ta phãi hiểu rõ mối quan hệ giữa các Bounded Context, Context Map là một kỹ thuật giúp chúng ta có thể hình dung mối quan hệ giữa các bối cảnh khác nhau từ đó có thể chọn các Integration Partern tốt nhất để giao tiếp giữa các Context.

Trong bài viết này chúng ta cùng thảo luận về quan hệ và cách tổ chức và tích hợp các các Bounded Context, chúng ta sẻ cùng nhau trả lời một số câu hỏi quan trọng tồn tại trong quá trình phân tích dự án như sau:

Câu trả lời cho các câu hỏi trên sẻ được tổng hợp trong một thứ gọi là Context Map, thứ được hình dung bằng hình minh hoạ dưới đây:

Markdowm Image

A. Các Bounded Context có liên quan đến nhau như thế nào?

Trong các hệ thống lớn và phức tạp, rất ít các Bounded Context thật sự tách biệt một cách hoàn toàn. Đa số các Bounded Context sẽ có một hoặc nhiều mối quan hệ với các Bounded Context khác.

Việc xác định các mối quan hệ này có tầm quan trọng không chỉ về mặt kỹ thuật(quyết định các sub-system giao tiếp phối hợp với nhau như thế nào) mà còn quyết định cả cách chúng được phát triển(các team sẽ phối hợp với nhau ra sao).

Cách phổ biến nhất để xác định mối quan hệ giữa các Bounded Context là phân loại các Bounded Context thành các Upstream và Downstream Contexts. Để dễ hình dung thì hãy tưởng tượng các Bounded Context như các thành phố đặt bên cạnh một dòng sông. Các thành phố ở thượng nguồn (Upstream) sẻ đổ những gì muốn truyền cho các thành phố ở hạ lưu(Downstream) xuống sông, những thứ đó sẻ được đưa đến các thành phố ở hạ lưu và họ chỉ cần vớt lên từ dưới sông.

Markdowm Image

Rõ ràng các Context đôi lúc có thể vừa được coi là Upstream Context lẫn Downstream Context tuỳ vào vị trí của chúng.

Tuy nhiên việc chia thành các Upstream và Downstream contexts sẻ cho chúng ta một số ưu và nhược điểm. Rõ ràng một Upstream Context sẻ không phụ thuộc vào bất kỳ Downstream Context nào, nó sẻ tự do phát triển theo bất kỳ hướng nào. Tuy nhiên, bất kỳ thay đổi nào ở Upstream Context cũng có thể gây ảnh hưởng nghiêm trọng đối với các Downstream Context.

Cùng với đó thì một Downstream Context bị hạn chế bởi sự phụ thuộc của nó vào Upstream Context nhưng lại không phãi lo lắng vì những thay đổi của nó sẻ gây ảnh hướng cho các Downstream Context khác hay không, điều này giúp các nhà Developer tại Downstream Context sẻ tự do hơn các Developer tại Upstream Context muốn làm gì cũng phãi nhìn trước ngó sau.

Chúng ta có thể hình dung các mối quan hệ giữa các Context một cách trực quan như hình dưới.

Markdowm Image

Ok, chúng ta đã quyết định rằng các Context về cơ bản sẻ có mối quan hệ như trên, vậy sẻ phãi tổ chức chúng như thế nào? Tổ chức ở đây đôi lúc vừa là tổ chức cơ cấu team lẫn tổ chức các module code.

DDD cung cấp cho chúng ta các integration partern nhằm giúp tổ chức đúng đắn và logic hơn. Thực ra nếu không có DDD chúng ta cũng hoàn toàn có thể nghĩ đến và áp dụng những cách tổ chức, những integration partern mà DDD suggest. Các dự án mình tham gia điều tổ chức chức theo những ý tưởng đó. Tuy nhiên chúng ta cùng ngó lại xem có gì hay ho không nào.

B. Integration Patterns:

1. Partnership:

Markdowm Image

Khi chúng ta đối mặt với việc phát triển hai hay nhiều contexts mà output của từng context này đều depend lên các Conext khác, để delivery các modules/features bắt buộc các team thuộc các Context này phãi phối hợp chặt chẻ với nhau. Các interfaces, contracts, features giữa hai bên hay bất cứ thứ gì cần cho việc develop hai context này cần được plan và lên lịch một cách chính xác nhằm tối thiểu hoá các ảnh hưởng xấu đến quá trình phát triển chung. Mỗi một sai lầm nào trong quá trình planning và scheduling đều có thể gây ra deadlock/bottleneck trong quản lí, ảnh hưởng đến việc delivery các modules/features. Khi đó mối quan hệ giữa các Context này được gọi là Partnership.

2. Shared Kernel:

Markdowm Image

Trong quá trình phát triển các Contexts thì việc một Entity Model, một business function được sài đi sài lại ở nhiều Context khác nhau là điều khó tránh khỏi, nhằm tránh việc duplicate code giữa các Context bắt buộc chúng ta phãi chia sẻ dùng chung các thành phần đó, chúng ta gọi đó là các thành phần Shared Kernel. Trong quá trình phát triển các Shared Kernel có thể bị thay đổi bởi bất cứ team nào nhưng phãi lấy được sự nhất trí của các bên nhằm đảm bảo không gây ảnh hưởng đến context nào đang dùng Shared Kernel này. Vì tính chất đó của share kernel code, để đảm bảo không có sai xót ảnh hưởng nào chúng ta cần phãi cover code bằng automation test.

3. Customer-Supplier:

Markdowm Image

Đây là mối quan hệ rất phổ biến giữa 2 Bounded Contexts trong đó các Consumer Contexts(Downstream Contexts) sẻ phãi phụ thuộc vào Output của Supplier Contexts(Upstream Contexts). Các team ở Upstream Contexts có thể phát triển mà không phụ thuộc vào các team ở Downstream Contexts, trong lúc đó các team ở Downstream phãi phụ thuộc vào Downstream context. Trong thực tế nếu các team fair với nhau hơn họ có thể define ra các interface contract, cả Upstream và Downstream context phãi tuân thủ theo interface contract này do đó có thể phát triển song song mà không phãi phụ thuộc quá vào nhau.

Thông thường khi phát triển một hệ thống gồm nhiều Customers Suppliers, các yêu cầu từ các Customer Context nên được xem là Business Requirement thay vì Customer Context Requirement và chuyển cho quá trình planning, prioritizing ở Supplier Context.

4. Conformist

Markdowm Image

Các Bounded Context thường có mối quan hệ Upstream/Downstream với nhau tuy nhiên các team ở Upstream Context thường không có động lực để đáp ứng các nhu cầu của team ở Downstream Context đặc biệt là các third-party Context.

Lấy ví dụ như hai Context Shipping và Shipping Service trong một ứng dụng thương mại điện tử. Context Shipping ở đây là Shipping Context của ứng dụng, còn Shipping Service là Third Party Context, rõ ràng chúng ta có thể thấy rằng Context Shipping là Downstream Context phụ thuộc khá chặt với Upstream Context là các Shipping Service như GiaoHangNhanh, Grab, NINJA VAN….

Ứng với mỗi Client, khi trạng thái đơn hàng là SHIPPING thì tôi muốn kiểm tra xem khoản bao lâu nữa nó sẻ tới, rõ ràng chúng ta phãi request đến Shipping Service rồi. Ban đầu khi integrate với Shipping Service, họ sẻ cung cấp cho chúng ta một bộ SDK với các response model cụ thể, tuy nhiên chúng ta chỉ là một trong hàng ngàn khách hàng của họ thôi, bất cứ điều gì chi phối lên tình hình kinh doanh của họ đều có thể khiến họ thay đổi các model này khi đó integration giữa hệ thống của chúng ta và Shipping Service đó sẻ xãy ra lỗi. Trong tình huống này chúng ta phãi hoàn toàn chấp nhận Data Model mới của họ, bằng cách gì đó chúng ta phãi đáp ứng được những thay đổi đó. Anticorruption Layer sẻ giúp chúng ta chống lại những thay đổi đó.

5. Anticorruption Layer

Markdowm Image

Như đã chúng ta đã thảo luận về mối quan hệ Upstream-Downstream giữa các Context, các team ở Upstream Context(đặc biệt là các third party context) thường không care đến nhu cầu của Team ở Downstream Context đồng thời họ có quyền tự do thay đổi data model bất cứ lúc nào core business của họ yêu cầu.

Điều này gây ra một số vấn đề như làm invalid data input vào các Downstream Context. Đôi lúc vì quá phụ thuộc vào Data Model của Upstream context mà các Domain Model ở các Downstream Context không rõ ràng về mặt ngữ nghĩa, ví dụ như với một đơn hàng cần ship đi trong Shipping Context thì define là order tuy nhiên trong Shipping Service ở third party thì nó lại là booking chẳng hạn, nếu vì quá phụ thuộc mà chọn từ booking cho Shipping Domain Model thì dẫn đến không thực sự phản ánh đúng về mặt ngữ nghĩa.

Do đó để ngăn những thay đổi đó làm ảnh hưởng đến Domain Model trong các Downstream Context cũng như đảm bảo các thuật ngữ bên trong mỗi Context được trong sáng hơn, không bị xâm lấn bởi ngữ nghĩa của các Dependency Context, thay vì tuân thủ model hay bất cứ gì mà team ở Upstream Context define ra, team ở Downstream cần tạo ra một lớp trừu tượng nhằm cô lập Domain Model khỏi Dependency Model, lớp này chịu trách nhiệm translate data từ Domain Model sang Dependency Model và ngược lại, nó gọi là Anticorruption Layer.

Anticorruption Layer cho phép các thành viên trong team có thể làm việc với một Domain Model ứng với Context của họ, trong khi vẫn tích hợp với Upstream Context. Khi Upstream Context thay đổi thì chỉ cần chỉnh sửa DTO trong Anticorruption Layer chứ không cần thay đổi gì ở Domain Model, Business Logic của Downstream Context.

6. Open Host Service:

Một Bounded Context có thể mở một hoặc nhiều protocol(có thể là Rest/gRPC hay cả message bus) để bất kì Bounded Context khác muốn tích hợp/sử dụng service của nó đều có thể access thông qua các protocol này, chúng được hiểu là Open Host Service.

7. Published Language:

Việc translate ngữ nghĩa giữa các model của hai hoặc nhiều Bounded Context đòi hỏi giữa chúng phãi có một ngôn ngữ chung. Các Bounded Context sẻ phãi đồng thuận với nhau trên một ngôn ngữ chung ví dụ như các XML schema, UML, DSL,…dựa vào các language đó các Bounded Context có thể tương tác với nhau một cách chính xác hiệu quả. Các language đó gọi là Published Language. Với tính chất được chia sẻ giữa các Bounded Context, Published Language gần giống với Open Host Service và thường được sử dụng đi kèm với Open Host Service.

8. Separate Ways:

Chúng ta phải cứng rắn hơn và đôi lúc phãi tàn nhẫn hơn khi define các requirements. Nếu hai bộ chức năng không có mối quan hệ đáng kể thì chúng phãi được tổ chức tách biệt hoàn toàn. Integration luôn luôn có một cái giá rất đắt cho việc coupling trong lúc đôi khi chỉ mang lại một lợi ích nhỏ. Việc define, phân tách hoàn toàn các Bounded Context làm cho chúng không dính với các Bounded Context khác do đó cho phép Developers phát triển một cách đơn giản hơn, chuyên biệt hơn trong phạm vi nhỏ, dễ dàng optimize hơn và đở rối rắm hơn.

9. Vậy chúng sẻ phối hợp hoạt động như thế nào?

Markdowm Image

Markdowm Image

Markdowm Image

C. Các vấn đề khi design, tổ chức integrate các Bounded Context:

Trong quá trình làm việc trong một ứng dụng với DDD và Event Driven Approach mình gặp phãi một số vấn đề sau:

1. Xung đột trong process:

Khi implement một service với Event Driven Approach với Kafka, đó là một Context vừa đứng ở vị trí là Upstream của một số Context cũng vừa là Dowstream của một số Context mình có một số suy nghĩ mâu thuẩn sau:

2. Vấn đề request từ các Downstream Context:

Trong quá trình giải quyết sub domain của mình mà một số Downstream Context khác cứ kiên tục yêu cầu cái này cái khác khiến mình distract, những internal request này thực sự khó reject nhưng nó lại ngoài scope và khó lường.

Với size product còn chưa lớn(vài chục người) thì communication diễn ra khá là flexible chứ giả dụ như các Team lớn hơn, tách biệt thành các phòng ban khác nhau thì phãi làm sao.

Sau một thời gian, các intenal request đó được giải quyết bằng cách chuyển thành business request hết, đầu mỗi sprint các team phãi determine các dependency request đó rồi gửi cho manager đẩy xuống cho các team plan trước.

3. Vấn đề tổ chức luồng dữ liệu, event flow, concurrency issue handling(race condition), validate data between bounded contexts.

C. Conclusion:

Vấn đề phức tạp mà Context Mapping giải quyết cho các Architect và Manager là:

Các Context Maps có thể được sử dụng cho một góc nhìn toàn cảnh về cách chúng ta tổ chức, phát triển các thành phần trong một enterprise system. Nhìn vào đó chúng ta có thể thấy được những hạn chế về mặt architecture như kiến ​​trúc như các bottleneck trong việc integrate chẳng hạn. Các Context Maps giúp chúng ta có thể xác định được các vấn đề làm block quá trình phát triển hệ thống cũng như các vấn đề trong management mà chúng ta khó phát hiện ra nếu sử dụng các phương pháp khác.

(to be continued)

comments powered by Disqus
rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora