Domain Driven Design (Phần 1)

Domain Driven Design là gì?

Có lẽ chúng ta đã quá quen thuộc với cách tiếp cận truyền thống khi xây dựng một ứng dụng. Đầu tiền chúng ta đọc spec và tìm hiểu các chức năng, sau đó tiến hành chia nhỏ các task. Trong phần lớn trường hợp, việc này nhằm mục đích estimate thời gian và lên kế hoạch thực hiện cho các task này. Vậy trình tự công việc sẽ là estimate thời gian, chia việc cho các thành viên trong team, thiết kế cơ sở dữ liệu, cuối cùng là bắt tay và code. Đây là cách thiết kế hướng dữ liệu hay còn gọi là Data Driven Design.

OK? Vậy vấn đề với cách tiếp cận là gì nhỉ? Lâu này chúng ta vẫn tiếp cận theo cách này và vẫn làm tốt đấy chứ? Câu trả lời là Đúng và Sai. Đúng ở chỗ chúng ta vẫn làm tốt trong việc bàn giao project, Sai ở chỗ chúng ta chưa thực hiện tốt trong việc bảo trì và mở rộng project.

Trong các ứng dụng điển hình có rất nhiều phần code xử lý các task không liên quan đến logic nghiệp vụ như truy cập file, mạng hay database, các phần này thường được gọi là plumping code và được nhúng trực tiếp vào trong Business Object và nhiều Business Logic cũng được nhúng vào behavior của UI Widget hay script của database, điều này thường xảy ra vì nó làm chúng ta phát triển ứng dụng một cách nhanh chóng và dễ dàng. Việc này dẫn đến phần lớn thời gian phát triển ứng dụng của developer là dành cho việc viết các plumping code thay vì viết Business Logic thực sự, nó làm cho thiết kế của chúng ta bị mất đi tính hướng đối tượng trong thực tế.

Ngoài ra khi logic nghiệp vụ trộn lẫn với layer khác khiến cho việc đọc hiểu và suy nghĩ về logic của ứng dụng trở nên khó khăn hơn đối với những người ngoài. Chỉ một thay đổi nhỏ ở tầng UI cũng có thể dẫn tới việc thay đổi tầng logic và ngược lại khi thay đổi một business rule của ứng dụng đòi hỏi chúng ta phải quan tâm đến từng chi tiết nhỏ phía UI cũng như Database để đáp ứng được sự thay đổi này.

Trong những ứng dụng nhỏ thì vấn đề này chúng ta không nhìn thấy. Ở các ứng dụng cỡ vừa thì vấn đề này đã tồn tại và bắt đầu dẫn đến tình trạng phá vỡ các thiết kế chuẩn (Anti-pattern). Và đối với các ứng dụng lớn thì nó trở thành vấn đề nghiêm trọng, cách tiếp cận trên sẽ không thể cho chúng ta đưa ra một thiết kế hướng đối tượng chính xác. Giải pháp ở đây chính là Domain Driven Design (DDD).

Vậy DDD là gì? DDD không liên quan gì đến công nghệ hay framework là những thứ thuộc về tầng vật lý mà nó là một khái niệm thuộc về tầng logic khi chúng ta xây dựng một hệ thống phần mềm. Cụ thể hơn nó là một design pattern và hơn nữa đây là design pattern ở cấp độ kiến trúc của hệ thống (bạn có thể tìm hiểu thêm trong cuốn Patterns of Enterprise Application Architecture của Martin Fowler), chúng ta cần rõ điều này để phân biệt với các design pattern nổi tiếng ở cấp độ class. Nó cung cấp một cấu trúc thực hành (structure of practice) và các thuật ngữ (terminology) cho việc ra quyết định thiết kế nhằm tập trung và tăng tốc các dự án phần mềm trong các lĩnh vực phức tạp.

DDD được giới thiệu lần đầu tiên bởi Eric Evans, trong cuốn sách của ông với tựa đề: Domain-Driven Design - Tackling Complexity in the Heart of Software (Addison-Wesley Professional 2003). DDD là cách tiếp cận đến phát triển phần mềm cho phép các team quản lý cấu trúc và bảo tri phần mềm trong những lĩnh vực có độ phức tạp lớn.

Xây dựng kiến thức chung về domain

Để tạo ta một phần mềm tốt, bạn cần hiểu về phần mềm đó. Bạn không thể làm ra hệ thống phần mềm ngân hàng nếu trừ khi bạn có hiểu biết tương đối tốt về mảng ngân hàng và những điều liên quan. Nghĩa là, để làm phần mềm tốt, bạn cần hiểu lĩnh vực ngân hàng.

Liệu có thể làm được phần mềm ngân hàng phức tạp dù không có hiểu biết nghiệp vụ tốt? Không thể. Không bao giờ. Ai hiểu về banking? Người thiết kế phần mềm? Không. Đồng chí này chỉ tới ngân hàng để gửi tiền và rút tiền khi cần. Người phân tích phần mềm? Cũng không hẳn. Anh ta chỉ biết phân tích một chủ đề cụ thể khi anh ta có đầy đủ tất cả cấu phần. Lập trình viên? Quên chuyện đó đi. Vậy là ai? Nhân viên ngân hàng, hiển nhiên. Hiểu nhất về hệ thống ngân hàng là những người ở trong đó, những chuyên gia của họ. Họ hiểu mọi thứ chi tiết, cái hay-dở, mọi vấn đề có thể và mọi quy định. Đây là nơi chúng ta thường xuất phát: Lĩnh vực (domain).

Giả sự bạn cần xây dựng một hệ thống phần mềm quản lý bệnh viện. Rõ ràng bạn cần làm việc với đội ngũ bác sĩ, y tá (chính là các chuyên gia trong lĩnh vực này - domain expert) để xây dựng kiến thức về domain. Bạn và họ nói chuyện, trao đổi kiến thức, đặt câu hỏi và trả lời. Bạn cần hiểu rõ càng nhiều càng tốt về domain này. Bằng cách đặt câu hỏi đúng, xử lý thông tin đúng cách, bạn và chuyên gia sẽ dần vẽ ra một domain, một mô hình domain (domain model). Bạn là kỹ sư phần mềm, kết hợp với domain expert cùng tạo nên một domain model và mô hình đó là nơi kiến thức chuyên môn của cả hai bên được kết hợp và tổng hợp lại.

Hãy xem xét tiếp ví dụ sau. Giả sử bạn đang tham gia thiết kế một tòa nhà. Yêu cầu là:

  • Xây dựng trên một diện tích đất cố định
  • Tòa nhà cao 6 tầng
  • Mỗi tầng có 4 căn hộ

Vậy domain ở đây là gì? Là công trình xây dựng chăng? Cũng có thể. Nhưng nếu bạn xem công trình xây dựng là domain của bạn, thì có thể bạn đang bỏ qua một vài chi tiết trong yêu cầu. Công trình xây dựng bạn đang thiết kế phải bao gồm thiết kế căn hộ cho người dân sinh sống. Vậy thuật ngữ "công trình xây dựng" có thể khiến chúng ta bỏ lỡ chi tiết, thay vì đó ta có thể thu hẹp xuống thành "chung cư". Lúc này nếu bạn nói với các kỹ sư về việc thiết kế, rõ ràng thuật ngữ "chung cư" sẽ dễ hiểu hơn là "công trình xây dựng" đơn thuần. Bạn thấy đấy, chỉ một thay đổi nhỏ trong ngôn từ cũng có thể tạo nên sự khác biệt. Trở lại ví dụ phần mềm bệnh viện ở trên, bạn và các bác sĩ, y tá không thể nói cùng một ngôn ngữ được. Họ nói về từ ngữ chuyên môn, bạn nói bằng đối tượng, phương thức, quan hệ. Đó chính là lúc chúng ta cần một ngôn ngữ chung để cả hai bên có thể làm việc với nhau dễ dàng hơn.

Ubiquitous Language

Khái niệm rất đơn giản, bạn và domain expert phải cùng nói một ngôn ngữ chung để cùng hiểu đúng một vấn đề.

Ví dụ về Ubiquitous Language.

Sai: Tỉ lệ chiều dài, chiều rộng của một phòng ngủ cỡ nhỏ là 4:3.
Đúng: Chiều dài phòng ngủ trẻ em là 6m, chiều rộng là 4.5m.

Rõ ràng những từ như phòng ngủ cỡ nhỏ, tỉ lệ thiên hướng kỹ thuật. Phòng ngủ trẻ em sẽ dễ hiểu hơn, và một số đo cụ thể rõ ràng có ý nghĩa và dễ hình dung hơn.

Kiến trúc phân lớp

Như đã nói ở trên, khi các đoạn code liên quan đến nghiệp vụ được trộn lẫn giữa các tầng lại với nhau, nó trở nên vô cùng khó khăn cho việc đọc cũng như suy nghĩ về chúng. Các thay đổi ở giao diện người dùng cũng có thể thực sự thay đổi cả logic nghiệp vụ. Để thay đổi logic nghiệp vụ có thể yêu cầu tới truy vết tỉ mỉ các đoạn mã của giao diện người dùng, CSDL, hoặc các thành phần khác của chương trình. Mô hình phát triển hướng đối tượng trở nên phi thực tế. Do đó, hãy phân chia một chương trình phức tạp thành các LỚP. Phát triển một thiết kế cho mỗi LỚP để chúng trở nên gắn kết và chỉ phụ thuộc vào các tầng bên dưới. Dưới đây là giải pháp kiến trúc chung cho DDD.

layered architecture.jpg

Ở đây mô hình DDD vẫn giữ lại những ưu điểm của mô hình kiến trúc phân lớp (Layered Archiecture) để đảm bảo nguyên lý Seperation of Concerns. Các phần logic xử lý khác nhau sẽ được cô lập ra khỏi các phần khác làm tăng tính Lose Coupling của ứng dụng và tính dễ đọc và dễ bảo trì cũng như ứng dụng khi có thay đổi logic của từng layer thì không ảnh hướng đến các layer khác.

User Interface: Chịu trách nhiệm trình bày thông tin tới người sử dụng và thông dịch lệnh của người dùng. Có thể hiểu là các sự kiện xảy ra trên giao diện khi được trigger sẽ được dịch thành lệnh và xử lý ở các tầng dưới.

Applicatioin Layer: Tầng này được thiết kế khá mỏng (ít xử lý logic) phối hợp các hoạt động của ứng dụng. Nó không chứa logic nghiệp vụ. Nó không lưu giữ trạng thái của các đối tượng nghiệp vụ nhưng nó có thể giữ trạng thái một tiến trình của ứng dụng. Chúng ta có thể hình dung phần này gần giống với các Controller trong mô hình MVC chỉ làm nhiệm vụ chuyển tiếp các task đến nơi cần xử lý.

Domain Layer: Tầng này chứa thông tin về các lĩnh vực. Đây chính là trái tim của phần mềm. Trạng thái của đối tượng nghiệp vụ được giữ tại đây.

Infrastructure Layer: Tầng này đóng vai trò như một thư viện hỗ trợ cho tất cả các tầng còn lại. Nó cung cấp thông tin liên lạc giữa các lớp, cung cấp chức năng lưu trữ các đối tượng nghiệp vụ, chứa các thư viện hỗ trợ cho tầng giao diện người dùng...

Đến đây thì chúng ta sẽ thấy kiến trúc của DDD tuy mới nhìn có vẻ lạ nhưng chỉ đơn giản là nó tùy biến lại mô hình kiến trúc 3 lớp (3-tier architecture) cho linh hoạt hơn. Tính linh hoạt này được tạo ra từ hệ quả của việc tái tổ chức lại các layer từ mô hình ba lớp, nó thể hiện ở data flow và control flow giữa 2 mô hình.

3-tier Architure

Chúng ta có thể thấy là trong mô hình 3 lớp thì tầng trên sẽ phụ thuộc trực tiếp vào tầng dưới nên không thể truy cập dữ liệu một cách trực tiếp từ tầng Presentation sang tầng Data Access Layer mà không thông qua tầng Business Layer. Còn mô hình DDD thì từ tầng User Interface nếu muốn lưu cái gì đó vào trong database chẳng hạn nó có thể gọi trực tiếp xuống tầng Infrastructure để làm được việc đó. Rõ ràng là trong kiến trúc DDD thì tính lose coupling được đảm bảo tốt hơn. Có thể hình dung một cách trực quan là mô hình 3 lớp giống như một ngôi nhà 3 tầng chỉ có cầu thang bộ, và việc di chuyển giữa tầng một và tầng 3 cần đi qua sàn tầng 2, trong khi mô hình DDD giống như ngôi nhà 4 tầng có lắp thêm thang máy, chúng ta có thể di chuyển đến các tầng khác nhau một cách tự do hơn.

OK. Bài viết này mình dừng lại ở phần kiến trúc của DDD. Phần sau mình sẽ trình bày về Building Blocks, tương tự như những viên gạch để xây dựng nên kiến trúc này.

Thank you for reading.

Tham khảo: