Thói quen phổ biến
Có một xu hướng phổ biến hiện nay là tập trung vào dữ liệu thay vì domain. Thay vì thiết kế các khái niệm domain với những hành vi nghiệp vụ phong phú thì chúng ta lại chỉ nghĩ về những dữ liệu gì cần được lưu trong database cùng với các mối quan hệ giữa chúng thể hiện qua các foreign key.
Trong một số bài viết, tôi có nhắc về thói quen (dẫn tới phổ biến) hiện nay khi xây dựng phần logic nghiệp vụ của ứng dụng, đó là sử dụng Entity dưới dạng data class và logic triển khai tại Service class dưới dạng Transaction Script. Những entity như vậy được Martin Fowler đặt tên là Anemic Domain Model.
Nhắc lại về một trong những ví dụ đó. Class Account có thiết kế như sau:

Account trông giống một domain model thật sự. Nó được đặt tên theo nghiệp vụ của ứng dụng: Khi trao đổi nghiệp vụ, mọi người dùng từ “Account”, và cho biết Account bao gồm những thuộc tính
nào. Đây là cách sử dụng Ubiquitous Language mà DDD đề cập. Những class khác cũng tương tự như vậy. Ngoài ra, chúng còn có thể có cấu trúc và các mối quan hệ phong phú như một mô hình domain thực thụ.
Nhưng có một thứ chúng thiếu hoặc rất không đầy đủ (khi chỉ có một vài hành vi nhỏ lẻ trong tập hành vi phong phú của đối tượng): hành vi nghiệp vụ. Chính vì thế, trong nhiều bài viết, chúng ta thường gọi chúng là những data class. Nếu bạn còn nhớ, trong bài viết Transaction Script, tôi có nói về một phần lý do của hiện tượng này, đó là di sản từ mô hình 2 lớp (từ thế hệ 7x, 8x) hoặc người thiết kế không biết về domain model.
Nếu như trong domain model, logic nghiệp vụ được đặt tại hành vi của domain entity, thì ở đây, chúng nằm tại các đối tượng service. Mô típ chung là service object tiếp nhận input, lấy data classes từ database lên để tính toán nghiệp vụ rồi lưu chúng lại.
// Gán tài xế cho đơn hàng và lưu ID của đơn hàng cho tài xế
shipment.setDriverID(nearestDriver.getDriverID());
nearestDriver.addShipmentID(shipment.getShipmentID());
// Tính phí giao hàng dựa trên khoảng cách
double fee = minDistance * COST_PER_KM;
shipment.setFee(fee); // Thiết lập phí giao hàng cho đơn hàng
// Cập nhật trạng thái đơn hàng là ASSIGNED
shipment.setStatus(ShipmentStatus.ASSIGNED);
Trong đoạn code trên (xem Transaction Script), Shipment, Driver là những phương tiện vận chuyển dữ liệu để service thể hiện logic thông qua những phương thức get/set.
Những “giả” entity như vậy được gọi là Anemic Domain Model – là một anti-pattern. Nhìn qua tưởng chừng như đó chỉ là vấn đề của sự lựa chọn: chọn giữa Anemic hay Domain Model mà thôi. Tôi lại thấy một hệ lụy đáng kể khi mà nó làm mất đi “suy nghĩ” hướng đối tượng của lập trình viên, và rồi các ý nghĩa thật sự của thiết kế hướng đối tượng đã bị họ bỏ lỡ. Các chi phí mà nó mang tới là toàn bộ những gì mà một domain model cũng cần chịu nhưng giá trị có được lại cực kỳ thấp.
Trong ví dụ ở Transaction Script, ta vẫn thấy code sau khi refactoring, các domain model chứa dữ liệu và hành vi nghiệp vụ, còn Service Layer – hay chính xác hơn theo cách gọi của chúng ta là Use Case Layer đảm nhận vai trò điều phối giữa các đối tượng domain object đó và giao tiếp với bên ngoài qua một cơ chế tổng quát, là các Output Ports.
“In general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, you've robbed yourself blind.”
Vun đắp thói quen mới
Để xây dựng thói quen hướng đối tượng, chúng ta sẽ cùng đi qua một số chủ đề nói về các cách thức hay công cụ để xác định, xây dựng những entity trong domain của bài toán.
Theo định nghĩa của Eric Evans, cha đẻ của phương pháp luận Domain-Driven Design, thì entity là đối tượng mang những đặc điểm sau:
- Entity là đối tượng trong domain của bài toán cần xây dựng, nó có tính định danh duy nhất thông qua một Identity, nhờ đó, nó có thể phân biệt với tất cả các đối tượng khác trong hệ thống. Đối với một đơn hàng, ID của nó là OrderID, đối với một tài khoản ngân hàng thì đó là AccountID,…
- Entity có thể thay đổi và thực tế nó cần phải thay đổi trong một thời gian dài. Các thay đổi có thể làm cho Entity biến đổi hoàn toàn so với ban đầu nhưng nó vẫn là chính nó, nhờ vào Identity. Khi tạo một đơn hàng, bạn có thể thay đổi nó nhiều lần qua các chỉnh sửa của khách hàng, qua các khâu từ đóng gói, vận chuyển, cho tới giao tới tay người nhận, đơn hàng vẫn được xác định là chính bản thân nó như lúc nó mới được tạo ra nhờ vào OrderID bất biến.
- Và như chúng ta nêu ra bên trên, Entity không chỉ mang dữ liệu thông qua thuộc tính bên trong mà nó còn chứa các hành vi nghiệp vụ. Những dữ liệu và hành vi này nên phù hợp trong một Bounded Context nhất định trong toàn domain bài toán. Sau này bạn sẽ thấy có những trường hợp mà có cùng những Entity với tên gọi giống nhau nhưng lại hoàn toàn khác nhau, vì chúng thuộc những Bouned Context khác nhau.

Hình 2 cho thấy cấu trúc của một Entity:
- Có định danh duy nhất
- Có các thuộc tính mô tả đặc điểm, là những Value Object
- Có hành vi nghiệp vụ mà thông qua đó thế giới bên ngoài có thể tương tác.
Vòng đời của một đối tượng nghiệp vụ
Giờ ta đã thấy, một đối tượng nghiệp vụ (có thể là Aggregates, Entities, Value Objects) có định danh, trải qua nhiều trạng thái khác nhau và nó còn đi đến hồi kết - bằng cách archive (lưu trữ vào kho) hoặc delete (xóa bỏ).
Vòng đời của đối tượng thì có dài, có ngắn. Những đối tượng ngắn có thể được tạo, sử dụng trong một phạm vi nhỏ rồi sau đó bị ngắt kết nối (không còn con trỏ nào trỏ tới nó) và trở thành ứng viên cho Gabage Collector. Các đối tượng này thông thường là những Value Objects của domain. Ngược lại, có những đối tượng lại có thời gian tồn tại lâu, thậm chí là vĩnh viễn. Trong lĩnh vực ngân hàng, ví dụ, dữ liệu giao dịch, báo cáo lạm phát có hiệu lực 5 năm. Với dữ liệu này, ngân hàng dường như sẽ phải để chúng trong vùng hot data - là nơi được sử dụng một cách chủ động. Với dữ liệu dài như vậy, không thể không có nơi lưu trữ. Và như vậy, ta cần một cơ chế lưu dữ liệu như một database. Dữ liệu đã lưu rồi không thể an nghỉ được, trong những hoạt động kinh doanh chúng thường được lấy ra và được tái tạo lại hình dạng như trước khi lưu xuống để sao cho hoạt động của nó được xuyên suốt, không có bất kỳ lỗi không nhất quán nào xảy ra. Và cứ như thế, mỗi đối tượng có thể có nhiều chuyến "du lịch" qua lại giữa ứng dụng và database.
Và rồi, tùy theo chính sách về dữ liệu của tổ chức, đối tượng cần phải đi tới hồi kết. Đó là khi ta cần xóa hoặc archive chúng.
Dưới đây là bức tranh mô tả vòng đời của đối tượng trong hệ thống:

Các giai đoạn trong vòng đời này sẽ được các "building block" của DDD phụ trách, thể hiện trong hình dưới đây:

Trong series bài viết về DDD, chúng ta sẽ lần lượt đi về các khái niệm về các thành phần quản lý vòng đời trong bức tranh trên.
Nhưng để hé mở thêm một chút thông tin tổng quát, tôi sẽ tóm tắt "bộ công cụ" mà DDD cung cấp như sau:
- DDD là phương pháp phát triển hướng miền (nghiệp vụ), nên mọi thứ cần phải xoay quanh nghiệp vụ: Mọi thành viên từ chuyên gia nghiệp vụ - Domain Experts, POs, Solution Architects, Software Developers, Testers... đều sử dụng những từ ngữ chuyên ngành (Ubiquitous Language). Cố gắng mọi thứ, mọi sản phẩm đều thể hiện những khái niệm đó như trong tài liệu thiết kế, take notes các buổi họp, mã nguồn, kịch bản kiểm thử,...
- DDD cung cấp hai bộ công cụ phân tích chính cho hai cấp độ khác nhau:
- Strategic Design: Bộ công cụ phân tích chiến lược giúp chúng ta phân tích những yếu tố, thành phần ở mức tổng quan của hệ thống như phân hệ thống thành các subdomain nào, mỗi subdomain bao gồm những bounded context nào và cách các bounded context chia sẻ, giao tiếp với nhau,...
- Tactical Design: Bộ công cụ phân tích chiến thuật giúp ta phân tích chi tiết trong một bounded context, giúp chi tiết hóa thiết kế của nó, với việc xác định những Aggregates, Entities, Value Objects, Domain Services,... cùng những Invariants (bất biến, hay luật nghiệp vụ lõi) cần thực thi. Đây có thể là điểm phù hợp cho người mới bắt đầu làm về DDD. Và chúng ta cũng sẽ khởi đầu ở Tactical design tools trước.
- Sử dụng các buổi họp trao đổi EventStorming để nắm bắt nghiệp vụ và thiết kế hệ thống và quan trọng nhất là để các bên liên quan thống nhất nghiệp vụ, sử dụng cùng một ngôn ngữ Ubiquitous Language. EventStorming rất phù hợp với Agile Development.
Trên đây là giới thiệu sơ bộ về Entity – nhân vật chính của chúng ta trong chuỗi lý thuyết Domain-Driven Design.
Bài kế tiếp: Định danh của Entity (Identity).