Kiến trúc phân tầng trong Domain-Driven Design

Một cách tiếp cận DDD theo hướng thực hành

Bài viết này là phần thứ hai nằm trong blog series khai phá Domain-Driven Design. Bạn có thể bấm vào đây để xem danh sách toàn bộ các bài viết.

Thành thực mà nói, lượng kiến thức trong lĩnh vực DDD đồ sộ đến nỗi có thể làm nản lòng bất cứ ai trên hành trình khai phá. Với mục tiêu khích lệ tinh thần của những nhà leo núi, mình quyết định sẽ tạm bỏ qua hệ lý thuyết trong DDD để trình bày một ví dụ minh họa về việc áp dụng nó. Hy vọng qua đó giúp anh em developer phần nào hình dung được quá trình coding DDD và nhìn thấy kết quả thực tế sau khi áp dụng vào project. Sau bài viết này, nếu anh em thấy DDD quả thực phù hợp với bản thân thì hãy lấy đó làm động lực để trở lại phần lý thuyết và tiếp tục cuộc hành trình nhé.

Trong phần này:

  • Bức tranh toàn cảnh
  • Kiến trúc phân tầng
  • Ví dụ minh họa

Bức tranh toàn cảnh

DDD 360⁰ overview

Như đã đề cập, mình sẽ ko đi sâu vào hệ lý thuyết về DDD (vốn đã có rất… rất nhiều sách vở và tài liệu tham khảo trên mạng). Nhìn vào bản đồ mind map ở trên, các bạn chỉ cần nhớ mang máng các từ khóa để sau này có thể khảo cứu khi cần.

Tựu chung lại, DDD bao gồm các khái niệm, nguyên lý và các hướng dẫn thực hành nếu được áp dụng một cách đúng đắn sẽ giúp bạn viết ra một sản phẩm phần mềm thực sự “xuất sắc” mà ở đó:

  • Khía cạnh nghiệp vụ của bài toán mà phần mềm sinh ra để giải quyết được làm nổi bật (so với những đoạn code ko liên quan như: UI, Database, XML/JSON …)
  • Mọi thành viên trong team dự án (kể cả non-tech guy) giao tiếp hiệu quả hơn (thông qua ubiquitous language)
  • Mã nguồn trở nên dễ apply unit test, dễ maintain hơn, các tính năng mới có thể được bổ sung vào một cách dễ dàng hơn (đây sẽ là tiền đề cho việc sau này apply quy trình CI/CD vào project một cách thuận tiện hơn)
  • Và đương nhiên, hệ quả sẽ là các team member bỗng thấy “cuộc đời vẫn đẹp sao, tình yêu vẫn đẹp sao…” dù cho dự án có nghiệp vụ lớn và độ phức tạp cao

Kiến trúc phân tầng

Có thể các bạn sẽ thắc mắc “lại lý thuyết nữa ư, sao chưa thấy code demo…?” Xin anh em hãy cứ bĩnh tĩnh bởi đây sẽ là phần lý thuyết tiền đề rất quan trọng. Ngay cả trong trường hợp project của bạn ko sử dụng DDD, bạn vẫn có thể áp dụng KTPT nhằm đặt được một số lợi ích nêu trên.

Project demo đi kèm bài viết này chỉ sử dụng vài khái niệm trong DDD, phần lớn mã nguồn còn lại sẽ được lập trình dựa theo KTPT. Bởi vậy các bạn hãy đọc thật kỹ phần này để có thể hiểu được tường tận mã nguồn của project demo.

Layer overview

Luật liên kết tầng

Đầu tiên và trên hết các bạn cần nắm được luật liên kết giữa các tầng (hay còn gọi là data flow). Có thể nói đây chính là phần hồn của Layer Architecture (hoặc Clean Architecture). Có thể bạn đã từng nghe ở đâu đó là project này hay project kia cũng áp dụng KTPT nhưng rốt cuộc mã nguồn vẫn rối như đĩa mỳ spaghetti, rất khó debug, cực khó maintain… Mình xin đánh cược vào khả năng rất cao là việc áp dụng KTPT của project đó chưa tuân thủ các luật liên kết này.

Hãy tưởng tượng các layer trong KTPT là các khối hình cầu mà ở đó tầng lõi bên trong bị bao đóng hoàn toàn bởi tầng cao hơn bên ngoài:

  • Tầng càng ở sâu bên trong thì càng có tính trừu tượng hóa cao (tức là sẽ liên quan nhiều đến business concept của bài toán mà phần mềm sinh ra để giải quyết); Tầng càng ở bên ngoài thì càng có tính trừu tượng hóa thấp, tính cụ thể cao (tức là sẽ liên quan đến việc implement cụ thể của phần mềm, ví dụ implement kết nối đến MySQL/RabbitMQ, implement RestAPI endpoints…)
  • Giao tiếp giữa các thành phần trong cùng layer ko bị hạn chế
  • Giao tiếp từ thành phần nằm ở layer ngoài vào thành phần nằm ở layer trong chỉ được sử dụng cấu trúc dữ liệu đơn giản (sao cho thành phần bên trong có thể hiểu được); Các thành phần bên ngoài có thể tùy ý sử dụng các thành phần bên trong (ví như trong method của class nào đó nằm ở layer ngoài, có thể khai báo và khởi tạo object instance của class nào đó nằm ở layer trong)
  • Giao tiếp từ layer trong ra layer ngoài phải thông qua interface được định nghĩa ở layer trong

Ko quá khó hiểu phải ko nào? Các bạn có thể dễ dàng nhận ra 3 luật cuối là hệ quả suy ra từ luật đầu tiên.

Bạn có thể tùy ý lựa chọn số lượng tầng trong KTPT, miễn là các luật nêu trên được đảm bảo. Thậm chí trong thực tế, bạn có thể phá luật nếu điều đó mang lại lợi nhuận kinh tế cho Công ty. Tuy nhiên, việc phá luật không nên bị lạm dụng và nó phải được thông tin rõ ràng để cả team nắm được, tránh những rắc rối của việc hiểu nhầm mã nguồn về sau.

Một ví dụ về cách phân tầng

Sau đây mình sẽ lấy một ví dụ điển hình về các layer trong kiến trúc phân làm 3 tầng:

Layer example

Tầng Entities

Là tầng nằm ở phần lõi trong cùng, chứa mã nguồn (class, method…) đóng gói các khái niệm của domain model (xem thêm) và hầu như ít khi thay đổi trừ khi nghiệp vụ của bài toán cần thay đổi.

Xin lưu ý, model ở tầng này là domain model chứ ko phải là database model (hay persistence model). Sự phổ biến của các framework (Spring, Django…) dễ khiến chúng ta lầm tưởng 2 khái niệm này là một, tuy nhiên ko phải vậy. Database model là cách thức chúng ta lưu trữ domain model vào một database cụ thể nào đó. Tùy thuộc vào việc lựa chọn sử dụng hệ quản trị CSDL cụ thể mà cách thức thiết kế database model sẽ khác nhau sao cho các câu lệnh truy vấn CRUD được xử lý tối ưu nhất. Nói một cách chi tiết hơn, cùng một domain model có thể được lưu trữ nhiều cách khác nhau trong mỗi hệ quản trị CSDL khác nhau.

Vì là tầng trong cùng, nên mức độ trừu tượng hóa của mã nguồn tại đây sẽ là cao nhất. Tầng này sẽ ko biết về việc CSDL cụ thể nào sẽ được sử dụng, ko biết về định dạng dữ liệu gửi/nhận là XML hay JSON và cũng ko biết giao diện frontend sẽ là dạng Web hay WinForm…

Tầng Use cases

Tầng này chứa mã nguồn cài đặt các nghiệp vụ mà phần mềm của bạn sẽ cung cấp cho người dùng (ví dụ: nghiệp vụ đăng nhập, nghiệp vụ mua hàng…). Cụ thể hơn, khi viết code thực thi nghiệp vụ của phần mềm, chúng ta sẽ cần sử dụng các class ở tầng Entities. Bên cạnh đó các nghiệp vụ lớn có thể sử dụng các nghiệp vụ nhỏ hơn để thực thi.

Tầng External

Đây là tầng ngoài cùng, nơi sẽ implement các interface được định nghĩa ở các tầng trước đó. Chẳng hạn tầng Use cases định nghĩa interface để tương tác với CSDL thì tầng External sẽ chứa class implement interface đó để cài đặt kết nối với một hệ quản trị CSDL cụ thể (ví dụ MySQL/MongoDB…). Nếu tầng Use cases có tương tác với interface của hệ thống message queue nào đó thì tầng External sẽ có class implement một hệ thống message queue cụ thể ( ví dụ RabbitMQ/ZeroMQ…).

Ví dụ minh họa

User stories

Phần mềm sinh ra là để giải quyết một bài toán nào đó trong thực tế, vì vậy trước khi bắt tay vào code, chúng ta cần tìm hiểu xem bài toán đó là gì? KH mong đợi phần mềm sẽ giải quyết bài toán đó đến đâu?

Ở bài viết này, giả sử KH yêu cầu phát triển phần mềm Todos như sau:

  • User will be logged in by providing a username (no need password, no need to register)
  • User could manage one ore more todo task items
  • Each task’s state could be as follow: new -> doing -> done
  • To stay focused, user could only have 3 doing tasks at a time. But if he has finished at least 3 tasks in his lifetime usage, he could have up to 5 concurrent doing tasks
  • User could delete/remove any task in the list
  • No need to manage task creation time

Các bạn đừng vội thắc mắc nếu thấy vài yêu cầu có vẻ ko hợp lý lắm so với thực tế nhé. Vì mục tiêu ở đây là giữ cho mã nguồn đơn giản và focus vào Layer Architecture trong DDD nên user requirements sẽ được cải biến cho phù hợp.

Domain model

Sau khi lấy được nghiệp vụ từ KH như trên, SA (software architect) họp lại cả team để trao đổi, thảo luận, chém gió… và cho ra output là bản design domain model như sau:

Làm sao để SA có thể vẽ ra được bản design như vậy là một câu chuyện dài kỳ, giờ thì bạn chỉ cần biết rằng bản design đó sẽ là nguyên liệu đầu vào để anh em coder bắt tay vào coding (cụ thể hơn là coding tầng Entities)

Coding

Việc diễn giải mã nguồn chi tiết tại đây sẽ khiến bài viết rất dài, vậy nên mình xin phép được tạm dừng. Các bạn vào GitHub để xem chi tiết nhé (có một lưu ý nhỏ là bạn hãy chịu khó checkout từng commit để có thể thấy được vòng đời của mã nguồn được cải tiến sau mỗi bước như thế nào).

Ở bài viết tới mình sẽ chia sẻ chi tiết hơn về mã nguồn và sự thay đổi khi nghiệp vụ của phần mềm tiến hóa theo thời gian, cùng với đó là quá trình coding để giúp anh em hình dung về vòng lặp design — code —refactor trong phát triển phần mềm.

Cám ơn các bạn đã theo dõi! Hãy follow tài khoản này để nhận thông tin về các bài viết tiếp theo nhé.

Happy DDD \(^_^)/

Backend Leader @ Pingcom, Runner