Kiến trúc Hexagonal trong DDD

Trong những bài viết trước, tôi đã giới thiệu cho các bạn các khái niệm cơ bản về DDD. Chúng ta cũng đã biết rằng trong model của DDD có khái niệm về kiến trúc phân lớp. Nó bao gồm 4 lớp là UI, Application, Domain và Infrastructure.

Hôm nay tôi sẽ giới thiệu cho bạn về kiến trúc Hexagonal, là cách giúp biểu diễn những gì chúng ta nghĩ về code. Nó định nghĩa khái niệm, vai trò của các lớp và sau đó chỉ ra cách phân tách code giữa các lớp. Nó cũng giúp làm rõ khi nào, làm thế nào và tại sao chúng ta sử dụng các interface ( trong vô vàn các ý tưởng khác nhau).

Vấn đề của kiến trúc phân lớp trong DDD

Nào chúng ta hãy cũng xem lại kiến trúc phân lớp trong DDD

Hãy nhớ rằng theo kiến trúc phần lớp này thì chúng ta có 4 lớp UI, Application, Domain và Infrastrcuture. Các lớp trên có thể truy cập được xuống lớp bên dưới nhưng lớp dưới thì không thể truy cập được các lớp trên. Đây chính là vấn đề của lớp kiến trúc này, gây ra một số điểm khó hiểu trong qua trình code. Nó sẽ gây khó khăn cho việc chúng ta định nghĩa các abstract (interface), hoặc sự phân tách giữa các lớp không rõ ràng.

Trong DDD, Domain là lớp quan trọng nhất vì biểu diễn toàn bộ các nghiệp vụ logic của hệ thống. Mọi tương tác đều phải đi qua và thông qua Domain. Trong lập trình chúng ta có thể coi Domain như là một abstract hay interface. Nhưng chúng ta đã biết rằng, với kiến trúc phân lớp ở trên thì lớp dưới không được truy cập lớp trên, tức là nếu interface đặt ở Domain thì phần implement của nó phải nằm ở lớp trên như hình sau

Nhưng bạn thấy có ổn không? Chắc chắn là không rồi, tức là giờ đầy phần implement của Domain lại phải đặt ở lớp Application. Điều này là không hợp lý về khái niệm lớp Application trong DDD và nó biến cấu trúc code của chúng ta hỗn hợp không rõ ràng.

Trong lớp Domain, chúng ta đặt các Repository ở đây. Và chúng ta biết rằng Repository chỉ đóng vai trò là abstract hay interface mà thôi. Nghĩa là implement của Repository thì thường được thực thi ở trong Infrastructure. Tức là Repository đấy có thể tương tác với cơ sở dữ liệu hoặc tương tác với bên ngoài bằng API, tất cả việc này đều được quyết định và implement ở Infrastructure. Nhưng chúng ta biết rằng lớp Infrastructure trong DDD không thể truy cập vào lớp domain vì nó là lớp thấp hơn, chính vì thế nó gây cho chúng ta bối rối và khó khăn khi giải quyết vấn đề này.

Trong DDD, các lớp tốt nhất nên độc lập với nhau càng nhiều càng tốt, interface được định nghĩa ở lớp dưới không nên được implement ở lớp trên nó. Ví dụ đơn giản khi bạn viết test code ở lớp dưới thì lớp trên chúng ta không cần test lại nữa vì nó đã được test ở lớp dưới rồi, khi gọi lại các hàm ở lớp dưới chúng ta chỉ cần mock lại thôi, nó giúp tăng hiệu suất cũng như tính độc lập và linh động giữa các lớp.

Định nghĩa lại phân lớp

Làm thế nào để giải quyết vấn đề trên đây? Trong quyển Impement DDD, các nhóm tác giả đã chia sẽ rằng, sau khi bàn bạc họ đã đưa ra một giải pháp đó là chuyển lớp Infrastructure lên cao nhất, còn lớp domain giờ đây ở dưới cùng. Giờ đây lớp infrastructure có thể truy cập được các lớp còn lại, còn Domain phần quan trọng nhất trong DDD sẽ là lớp thấp nhất, giúp biểu diễn các nghiệp vụ logic của hệ thống. Mối tương quan giữa các lớp giờ đây sẽ như thế này

Giờ đây cấu trúc source code của chúng ta đơn giản hơn nhiều, chúng ta có thể định nghĩa các abstract hay interface ở Domain hay Application. Phần implement code của nó sẽ được thực hiện ở lớp infrastructure. Nó rất đơn giản và rõ ràng đúng không, đặc biệt là với lập trình viên đã rất quen với các lớp interface và implement của nó.

Kiến trúc Port và Adapter

Port là gì?

Port chính là điểm vào/ra bắt buộc với khách hàng đến/từ một ứng dụng. Trong rất nhiều ngôn ngữ, nó chính là một interface. Ví dụ nó có thể là một interface dùng để tìm kiếm trong một search engine. Trong ứng dụng của chúng ta, Port đóng vai trò như một interface như một điểm vào/ra mà không cần biết rằng nó được implement như thế nào. Nó sẽ được inject vào ứng dụng như một interface đã được định nghĩa sẵn

Adapter là gì?

Adapter là một class mà dùng để biến đổi một interface thành interface khác.

Ví dụ, một adapter implement một interface A và được inject (nhúng vào) trong môt interface B. Khi một adapter được khởi tạo nó được inject vào trong constructor của một object của interface B. Adapter này sau đó sẽ được inject (nhúng vào) bất kỳ chỗ nào mà interface A cần được dùng và nhận các request xử lý và thực hiện chúng bên trong object mà được implement bởi interface B.

Kiến trúc Hexagonal

Như ở phần trên chúng ta đã thấy rằng các lớp trong DDD được phân chia truy cập theo chiều dọc từ trên xuống dưới. Nhưng trong kiến trúc Hexagonal mà nó sử dụng Port và Adapter thì giờ đây các lớp của chúng ta sẽ được truy cập theo chiều ngang, gồm có 2 phía: bên trái và bên phải.

Chúng ta có 3 phần. Phần trên cùng là giao diện, tiếp đến là phần xử nghiệp vụ và cuối cùng là tương tác với Data. Nó sẽ được chuyển thành như sau

Giờ đây nó chuyển sang truy cập theo chiều ngang, được chia thành 2 bên và truy cập sẽ từ trái sang phải. Mỗi bên sẽ có nhiều điểm vào/ra khác nhau. Ví dụ API và UI sẽ nằm ở 2 điểm vào / ra khác nhau ở phía bên trái, trong khi ORM và Search engine nằm ở 2 điểm vào ra khác ở phía bên phải. Các điểm này được sắp xếp lại với nhau thành một hình lục giác (hexagonal), đó chính là lý do vì sao nó được gọi là Hexagonal

Nhìn vào hình trên thì bạn có thể thấy rằng kiến trúc Hexagonal được chia thành 2 phía. Nó được cấu trúc theo mô hình port và adapter. Các điểm truy cập vào/ra được gọi là các port. Bạn có thể hình dung rằng nó như ổ cắm điện, là nơi để các thiết bị cắm vào (inject).

Có hai kiểu Adapter khác nhau. Adapter bên trái đại diện cho UI, nó thường được gọi là Primary hay Driving Adapter, bởi vì nó là điểm khởi đầu cho các hành động của application, trong khi Adapter phía bên phải đại diện cho các kết nối đền các công cụ backend được gọi là Secondary hay Driven Adapters, bởi vì chúng luôn phản hồi lại các tương tác của Primary Adapters.

Dựa vào hình vẽ trên thì bạn có thể thấy rằng giờ đây lớp Domain là trung tâm và tiếp đến là lớp Application ở bên ngoài. Luồng truy cập của chúng ta sẽ từ trái sang phải. Ví dụ một API được gọi từ application ở phía bên trái để lấy thông tin dữ liệu từ DB thì nó sẽ gọi ở phía Primary và được phản hồi lại ở port DB ở phía bên phải. Giờ đây, Domain và Application sẽ đóng vai trò là interface và abstract. Inteface này sẽ được implement ở phía bên phải (second port) và sẽ được inject (nhúng vào) phía bên trái (primary port). Có nghĩa là Application sẽ gọi và sử dụng interface này, việc thu và xử lý sẽ được implement ở port bên phải.

Kết luận

Kiến trúc Hexagonal rất rõ ràng và dể hiểu đúng không? Nó sẽ hoạt động dựa trên các port và adapter. Các port sẽ được phân chia thành 2 phía: Một phía xử lý các kết nối vào là primary port, một phía xử lý các kết nối ra bên ngoài như database hoặc API là secondary port. Chúng sẽ tương tác với nhau thông qua lớp giữa gồm Domain và Application để giúp xử lý các nghiệp vụ và logic, đóng vai trò như một lớp abstract hay interface

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.