[Series System Design - Bài 5] Hexagonal Architecture & Domain-Driven Design (DDD): Cách bảo vệ "core logic" khỏi sự thay đổi của Framework và Database
Chào anh em, hãy thành thật với nhau nhé. Anh em đang code theo mô hình MVC đúng không?
Một ngày đẹp trời, anh em nhận một task: "Viết API kiểm tra xem thẻ hành khách có đủ điều kiện qua cổng soát vé (Gate) hay không". Anh em liền mở Controller ra, validate request, gọi thẳng Model (có thể là Eloquent nếu anh em xài Laravel), query database, check số dư thẻ, update trạng thái, rồi trả về JSON.
Nhanh gọn lẹ! Code chạy rẹt rẹt.
Nhưng 2 năm sau, hệ thống phình to. Công ty quyết định đổi từ MySQL sang MongoDB cho linh hoạt. Hoặc giám đốc kỹ thuật yêu cầu nâng cấp framework lên version mới nhất có breaking changes. Lúc này, anh em nhận ra logic trừ tiền, logic kiểm tra thẻ hợp lệ đã bị "trói chặt" (tight coupling) vào các hàm query của Database và các thư viện của Framework. Mở code ra sửa mà tay run bần bật, sửa một chỗ vỡ ba chỗ.
Đó là lúc anh em cần đến Hexagonal Architecture (Kiến trúc Lục giác) và Domain-Driven Design (DDD).
1. Trái tim của ứng dụng: Core Business Logic (Domain)
Logic nghiệp vụ cốt lõi (Domain) là những quy tắc kinh doanh bất di bất dịch, dù công nghệ có thay đổi thế nào.
Ví dụ với hệ thống Automatic Fare Collection (AFC) của tuyến Metro: Quy tắc "Thẻ phải có số dư lớn hơn hoặc bằng giá vé chặng đường thì mới mở cổng Gate" là một Core Logic. Quy tắc này không hề quan tâm bạn đang dùng MySQL hay PostgreSQL, nó không quan tâm khách quẹt thẻ thực tế hay nhân viên test qua Postman, nó cũng chẳng màng việc bạn code bằng PHP, Java hay Go.
Tội lỗi lớn nhất của developer là trộn lẫn Core Logic này với Framework và Database. Domain-Driven Design (DDD) dạy chúng ta rằng: Lớp Domain phải là trung tâm của vũ trụ. Nó không được phép có bất kỳ dependency (phụ thuộc) nào ra thế giới bên ngoài. Không import thư viện HTTP, không import ORM, không gì cả. Nó chỉ chứa các Entities (Thực thể thuần túy) và các quy tắc nghiệp vụ.
2. Hexagonal Architecture: Bức tường thành bảo vệ Domain
Để bảo vệ sự "thuần khiết" của Domain, Alistair Cockburn đã đề xuất Hexagonal Architecture (hay còn gọi là Ports & Adapters).
Hãy tưởng tượng ứng dụng của bạn là một hình lục giác. Ở chính giữa tâm là Domain. Bao bọc xung quanh nó là các Ports (Cổng kết nối). Và ngoài cùng là các Adapters (Bộ chuyển đổi).
- Ports (Cổng): Là các Interface định nghĩa những gì Domain cần hoặc những gì Domain cung cấp.
- Inbound Port: Interface định nghĩa Use Case (Ví dụ:
ProcessTicketUseCasevới hàmexecute()). - Outbound Port: Interface định nghĩa việc lấy/lưu dữ liệu (Ví dụ:
TicketRepositoryInterfacevới hàmfindTicketById()). Domain chỉ biết nó cần một cái thẻ, nó không biết cái thẻ đó lấy từ đâu. - Adapters (Bộ chuyển đổi): Là những đoạn code cắm vào các Ports đó để giao tiếp với thế giới thực.
- Inbound Adapter: Nơi nhận request từ bên ngoài và gọi vào Inbound Port (Ví dụ: HTTP REST Controller, GraphQL, hoặc một CLI Console Command).
- Outbound Adapter: Nơi thực thi Outbound Port để tương tác với hạ tầng (Ví dụ:
MySQLTicketRepositorysử dụng ORM để query database, hoặcRedisTicketRepository, hoặc thậm chí gọi qua một API của bên thứ 3 như Hitachi).
3. Sức mạnh của sự đánh đổi
Anh em sẽ hỏi: "Viết như vậy thì code dài dòng quá, vẽ vời ra hàng đống Interfaces và thư mục làm gì?"
Đúng, Hexagonal Architecture đi kèm với cái giá là nhiều boilerplate code hơn. Bạn không thể chọc thẳng vào database từ Controller nữa. Nhưng bù lại, bạn có một "siêu năng lực":
- Testability (Dễ dàng Unit Test): Bạn có thể test toàn bộ logic trừ tiền vé, check cổng, đối soát giao dịch một cách hoàn hảo mà không cần phải bật database hay web server lên. Chỉ cần mock cái
TicketRepositoryInterfacelà xong. Test chạy tính bằng mili-giây. - Delay Decisions (Trì hoãn quyết định công nghệ): Bạn có thể bắt đầu code logic nghiệp vụ cốt lõi ngay lập tức với một cái
InMemoryTicketRepository(lưu data vào một mảng tạm). Vài tuần sau, khi chốt xong dùng database gì, bạn chỉ cần viết thêm một cái Adapter cắm vào, mọi logic bên trong không cần thay đổi dù chỉ một dòng. - Thay thế hạ tầng dễ dàng: Nếu Service quản lý Thẻ bị nghẽn, và bạn quyết định tách nó ra thành một Microservice riêng. Code ở Core Domain vẫn giữ nguyên! Bạn chỉ cần bỏ cái
MySQLAdaptercũ, và thay bằng mộtHttpClientAdapterđể gọi sang Microservice mới qua mạng. Mọi thứ vẫn "cắm và chạy" (plug and play) hoàn hảo.
Lời kết
Hexagonal Architecture không phải là viên đạn bạc cho mọi dự án. Với một trang web CRUD (Tạo/Đọc/Sửa/Xóa) đơn giản, dùng nó là "lấy dao mổ trâu giết gà". Nhưng với những hệ thống phức tạp, vòng đời dài, nơi logic nghiệp vụ là tài sản lớn nhất (như hệ thống tài chính, e-commerce, hệ thống lõi doanh nghiệp), thì đây là chiếc áo giáp vững chắc nhất giúp mã nguồn của bạn không bị mục nát theo thời gian.
Khi hệ thống đã được chia tách rõ ràng, các service đã sẵn sàng, một bài toán đau đầu khác lại hiện ra: Người dùng (Client - Mobile/Web) sẽ gọi các service này như thế nào? Chẳng lẽ App Mobile phải nhớ địa chỉ IP của hàng chục service khác nhau sao?
👉 Ở bài tiếp theo, chúng ta sẽ giải quyết mớ bòng bong này với: "API Gateway & BFF (Backend for Frontend): Quản lý luồng giao tiếp chằng chịt giữa Client và các Services."
Anh em thấy kiến trúc Lục giác này thế nào? Đã có ai từng "đau đớn" đập bỏ cả project chỉ vì framework ra phiên bản mới chưa? Hãy chia sẻ câu chuyện của anh em dưới phần bình luận nhé!
All Rights Reserved