I'm Anh, from 🇻🇳 and be a 👨💻, like to build a system by DDD.
Currently have interested in System Design 👷 🏗️ and Database.
I'm Anh, from 🇻🇳 and be a 👨💻, like to build a system by DDD.
Currently have interested in System Design 👷 🏗️ and Database.
Việc viết các class để từ đó tạo ra các object hiển thị các giá trị đặc trưng của hệ thống là một việc rất quen thuộc.
Việc phân chia, phân tán code dường như ít được để ý đến ở thời điểm hiện tại. Việc định nghĩa nhiều class value-object như vậy sẽ làm tăng số lượng các files định nghĩa class. Ban đầu, rất ít người có thể vượt qua được trở ngại này.
Vậy nên để vượt qua điều đó, chúng ta phải nắm được mục đích chính của bản thân khi sử dụng value-object
là gì.
Có thể liệt kê 4 mục đích cơ bản như sau:
Các mã này được tạo nên từ "chữ" và"số". Nếu chỉ đơn thuần sử dụng kiểu dữ liệu string
cho chúng
const serialNumber = "abc-123-xyz";
Khi đó nếu đọc code, chúng ta sẽ không thể rõ được đây là loại mã gì. Đồng thời việc tìm ra cấu trúc của nó cũng như trả lời cho câu hỏi nó được tạo ra như thế nào là vô cùng khó khăn, nếu ta định nghĩa value-object, mọi chuyện sẽ dễ dàng hơn nhiều.
class SerialCode {
private string productCode;
private string lotNumber;
private string branch;
}
So với việc một "string" duy nhất thì việc sử dụng value-object có ý nghĩa hơn rất nhiều. Ngoài ra đây cũng là một cách "viết doc" cho hệ thống.
userName
, ở phía người dùng nó chỉ đơn thuần là một string
. Nhưng ở phía hệ thống sẽ có những quy tắc như:Lấy ví dụ:
const userName = "me";
Nếu thiết kế hệ thống yêu cầu độ dài tối thiểu của userName
là 3, đoạn code trên vẫn được compile và chạy bình thường nhưng về mặt logic thì hoàn toàn sai. Ta có thể thêm code kiểm tra điều kiện
if (userName.length < 3) {
} else {
// throw exception
}
Việc này không hề khó nhưng càng thêm nhiều điều kiện thì code sẽ cồng kềnh và nếu điều kiện mới thêm không chính xác sẽ gây hại cho hệ thống.
Nếu sử dụng value-object, ta sẽ có class như sau:
class UserName {
private readonly value;
constructor (value) {
if (!value || value.length < 3) {
throw Exception
}
this.value = value;
}
}
class User {
private readonly id;
constructor(name: string) {
this.name = id;
}
}
Đoạn code này không sai, có những hệ thống sử dụng name
, email
làm id. Đối với người viết code thì không vấn đề gì, nhưng với người đọc code sau này có thể gây ra sự hiểu nhầm rằng "đoạn code này liệu có chính xác ?". Đọc code là không đủ mà cần xem xét đến thiết kế của hệ thống để biết được liệu nó có chính xác hay không.
Giải pháp ở đây vẫn là value-object
class UserName {
private readonly value;
}
class UserId {
private readonly value;
}
class User {
private readonly id;
private readonly name;
constructor(UserName name) {
this.id = name; // compiler error
}
}
Dù UserName
và UserId
chỉ là lớp bao của string nhưng với User
class nếu gán nhầm thì sẽ phát sinh lỗi ngay từ lúc compile.
Bản thân chương trình luôn có các lỗi tiềm tàng mà chỉ đọc code thôi ta không thể biết được, chỉ khi nào thực thi code mọi thứ mới trở nên rõ ràng. Việc sử dụng value-object
này giúp giảm đi các lỗi tiềm tàng, qua đó người bảo trì hệ thống sau này sẽ không quá "vất vả" trong việc vận hành và bảo trì hệ thống.
Với các logic kiểm tra điều kiện, nó có thể bị lặp lại ở nhiều nơi trong project, nếu điều kiện kiểm tra thay đổi ta sẽ phải thay đổi ở nhiều chỗ, việc làm này khá vất vả đặc biệt là với các hệ thống lớn.
Các logic thế này chỉ nên tập trung ở một chỗ
class UserName {
private readonly value;
constructor(name: string) {
if (!name || name.length < 3) throw Exception;
this.value = name;
}
}
class User {
private readonly UserName name;
constructor(name: string) {
const userName = new UserName(name);
this.name = userName;
}
updateName(newName: string) {
const newUserName = new UserName(newName);
this.name = newUserName;
}
}
Nếu điều kiện của userName
có thay đổi thì ta chỉ cần thay đổi ở một chỗ duy nhất là được.
Trong thực tế, hàng hoá từ kho được xuất ra không bao giờ đến trực tiếp tay người nhận ngay lập tức mà thường qua các kho trung gian
Thử mô hình hoá quá trình vận chuyển này bằng code.
Kho hàng là một thuật ngữ của domain, nên ta sẽ định nghĩa nó thành một Entity
class
class PhysicalDistributionBase {
public Baggage ship (Baggage baggage) {}
public void receive (Baggage baggage) {}
public void transport(PhysicalDistributionBase to, Baggage baggage) {
const shippedBaggage = ship(baggage);
to.receive(shippedBaggage);
}
}
Việc viết phương thức transport
vào entity
class trông có vẻ không ổn cho lắm.
Nếu định nghĩa transport
method vào domain service
, ta sẽ có service class như sau:
class TransportService {
public void transport (PhysicalDistributionBase from, PhysicalDistributionBase to, Baggage baggage) {
const shippedBaggage = from.ship(baggage);
to.receive(shippedBaggage);
}
}
Kinh nghiệm ở đây đó là các method cảm giác "không vừa" với entity
nên được cho sang domain service
.
Một vài cách đặt tên cho Domain service class:
DomainName
VD: User
DomainName + Service
VD: UserService. Đây là cách làm phổ biến nhất, trong class này sẽ là tập hợp các hàm service, tuy nhiên nếu tách riêng từng hàm ra thành các class riêng rẽ thì có thể đặt tên class gần với chức năng của hàm (VD: CheckDuplicateUserService
)
DomainName + DomainService
VD: UserDomainService.
Xét ví dụ
console.log(0 === 0);
console.log(1 === 0);
Đây là 2 ví dụ so sánh giá trị, việc so sánh ở đây sẽ được hiểu theo nghĩa so sánh từng thuộc tính cấu thánh nên giá trị
chứ không phải là so sánh trực tiếp giá trị với giá trị
, việc so sánh các value-object của hệ thống cũng tương tự như vậy.
var nameA = new FullName("ABC", "XYZ");
var nameB = new FullName("ABC", "XYZ");
console.log(nameA.isEqual(nameB));
Chúng ta cũng có thể so sánh bằng cách "tự tay" so sánh từng thuộc tính một
var nameA = new FullName("ABC", "XYZ");
var nameB = new FullName("ABC", "XYZ");
console.log(nameA.firstName === nameB.firstName);
console.log(nameA.lastName === nameB.lastName);
Cách này không sai nhưng nhìn khá "mất tự nhiên", nếu coi value-object của hệ thống giống như một loại "value" thì khi so sánh các giá trị nguyên thuỷ, code sẽ trông như sau:
console.log(1.value === 0.value)
Khá hài hước khi lấy ra "value" của "value".
Tương tự như ví dụ UserApplicationService
ở trên, ta sử dụng một object có tên là ServiceLocator
để lấy về instance phù hợp.
class UserApplicationService {
private readonly IUserRepository userRepository;
constructor() {
this.userRepository = ServiceLocator.Resolve<IUserRepository>();
}
}
Trước đấy ở startup script, ta cần đăng kí sẽ trả về instance loại nào
ServiceLocator.Register<IUserRepository, InMemoryRepository>();
Nếu cho môi trường production, ta sẽ đăng kí như sau:
ServiceLocator.Register<IUserRepository, UserRepository>();
Khi đó nếu switch giữa môi trường production
vs development
ta chỉ cần thay đổi ở startup script
là đủ.
Với việc sử dụng Service Locator
như trên ta có thể thiết lập cho việc switch giữa các môi trường với nhau.
Về bản chất nó là Domain Object thực thi Domain Model.
Ở chương nói về value-object, ta thấy value-object cũng là Domain Object, vậy value-object và Entity khác nhau ở điểm nào, câu trả lời đó là identity.
Lấy ví dụ với con người: mỗi người đều có tên, tuổi, chiêu cao, cân nặng, ... Sau một năm tuổi, chiều cao, cân nặng đều có sự thay đổi nhưng người nào vẫn là người đấy -> Đây chính là nhờ identity
Trong phần mềm, có những object không thể phân biệt bằng thuộc tính (VD: người dùng hệ thống - khi thay đổi thông tin cá nhân thì người dùng đó vẫn là người dùng đó -> phân biệt user bằng identity)
Với value-object thì yếu tố quan trọng nhất đó là định nghĩa được một "hành động". Lấy ví dụ về "Money Value-Object".
Với tiền thì sẽ có thuộc tính quan trọng đó là "số lượng" và "đơn vị" ($, ...).
class Money {
private readonly number amount;
private readonly string currency;
constructor(number amount, string currency) {
this.amount = amount;
this.currency = currency;
}
}
Value-Object không chỉ đơn thuần chứa dữ liệu mà nó còn có thể có thêm các methods khác. Ví dụ "tiền" ở trên, ta có thể thêm method Add
class Money {
private readonly number amount;
private readonly string currency;
constructor(number amount, string currency) {
this.amount = amount;
this.currency = currency;
}
public add(Money args) {
return new Money(this.amount + args.amount, currency);
}
}
Vì Value-Object là không thể thay đổi nên ở method Add
chúng ta trả về một instance mới. Kết quả trả về sẽ được đưa vào một biến mới.
const money = new Money(1000, "$")
const allowance = new Money(2000, "$")
const result = money.add(allowance);
Xử lí ở trên hoàn toàn tương tự với xử lí tính toán cho các kiểu dữ liệu nguyên thuỷ.
const str1 = "str1";
const str2 = "str2";
const res = str1 + str2;
Những phương thức không được định nghĩa: ở trên ta có phương thức thêm tiền được định nghĩa nhưng phương thức nhân tiền lại không được định nghĩa. Khi đó trong phần định nghĩa của value-object ta chỉ đưa ra function signature
mà thôi.
class Money {
public Money multiply(Money args);
}
Trong Application Service
thì không nên có sự xuất hiện của domain rules
.
Với ví dụ về Đăng kí User
hoặc Cập nhật thông tin User
, nếu ta tiến hành check duplicate user ở cả 2 usecases này thì trong trường hợp domain rule thay đổi (không trùng tên -> không trùng email) thì 2 usecases này cũng phải sửa đổi theo.
Với một hệ thống có User, thì từ khi User được tạo cho đến khi người dùng không sử dụng hệ thống nữa thì User sẽ bị xoá đi - Đây là vòng đời của User.
Là object thực thi Usecase
.
Giả sử một hệ thống có chức năng: Đăng kí user, Thay đổi thông tin user. Lúc này 2 chức năng này sẽ được coi là 2 usecase
và sẽ có 2 method tương ứng được định nghĩa trong Application Service
để triển khai. Thực tế thì 2 method sẽ sẽ sử dụng Domain Object
để triển khai.
Những pattern cần biết khi triển khai ứng dụng:
Các pattern khác cần biết:
let a = 1;
a.changeTo(2) // a: 1-> 2
"Hello".changeTo("HaHa") // NG
Ta có thể thay đổi giá trị của biến nhưng không thể thay đổi giá trị của "MỘT GIÁ TRỊ"
Mức độ gắn kết ở đây được hiểu là phạm vi mà module có thể bao quát, tập trung được.
Để đo mức độ gắn kết ta có "Lack of Cohesion in Methods - (LCOM)" . Được tính dựa theo tỉ lệ giữa: tổng số biến instance và số biến instance được các methods sử dụng.
Cùng lấy ví dụ về "mức độ gắn kết cao" và "mức độ gắn kết thấp".
class LowCohesion {
private number v1;
private number v2;
private number v3;
private number v4;
number methodA() {
return v1 + v2;
}
number methodB() {
return v3 + v4;
}
}
Ở class LowCohesion
thì methodA chỉ sử dụng v1
, v2
chứ không sử dụng v3
, v4
. Điều tương tự cũng xảy ra với methodB
.
Nếu tách thành 2 class như sau:
class HighCohesionA {
private number v1;
private number v2;
number methodA() {
return v1 + v2;
}
}
class HighCohesionB {
private number v3;
private number v4;
number methodB() {
return v3 + v4;
}
}
Mỗi method của class đều sử dụng toàn bộ các thuộc tính của class.
ApplicationService với mức độ gắn kết thấp
Với ví du về UserApplication
ở các phần trước, ta thấy chỉ có register
method là sử dụng cả userRepository
và userService
còn delete
method thì chỉ sử dụng userRepository
vậy nên theo quan điểm về mức độ gắn kết thì cách viết này cho mức độ gắn kết thấp. Ta có thể tách thành 2 class UserRegisterService
và UserDeleteService
Do tách class nên để tập hợp chúng lại, ta cần sử dụng package
VD:
Khi mọi xử lí liên quan đến user đều có thể được tìm thấy trong folder User
thì sẽ rất dễ dàng cho dev sau này.
Nếu ta viết toàn bộ các phương thức vào trong Domain Service
thì class UserService
sẽ trông như sau:
class UserService {
changeName(User user, UserName name) {
user.name = name;
}
}
Nếu viết toàn bộ các phương thức vào trong Domain Service
thì Entity
sẽ chỉ còn setter
và getter
. Khi đó nếu đọc code của Entity sẽ khá khó nắm bắt các phương thức của entity cũng như khó nắm bắt được các Domain Rules
. Lúc này entity chỉ có một nhiệm vụ duy nhất đó là lưu trữ dữ liệu
.
Khi đó chiến lược đóng gói Dữ liệu
và Phương thức
vào trong Object sẽ hoàn toàn bị phá sản.
Lúc này phương thức changeName
nên được viết vào UserEntity
class.
Cố gắng tránh lạm dụng Domain Service
Nếu ban đầu vẫn còn phân vân về việc nên viết vào Entity
hoặc Value-Object
hay là viết vào Domain Service
thì hãy viết vào Entity
hoặc Value-Object
.
Hãy cố gắng tránh việc viết vào Domain Service nhiều nhất có thể
Việc lạm dụng Domain Service
sẽ làm cho logic sẽ bị phân tán đi nhiều chỗ.
Hiểu nôm na thì Service là
Một thứ được sinh ra để phục vụ người dùng
Domain Service là object thể hiện nghiệp vụ của một lĩnh vực nào đó.
Application Service được tạo ra để giải quyết một vấn đề nào đó của người dùng.
VD:
Các tính năng như UserRegister hay UserWithdraw không hề thuộc về một domain nào cả, nó thuộc về ứng dụng nên đó là những Application Services.
Service không hề có trạng thái
Ví dụ với UserApplicationService
class UserApplicationService {
private readonly userRepository: IUserRepository;
}
Rõ ràng UserApplicationService
có "trạng thái" là userRepository
nhưng userRepository
không hề thay đổi trực tiếp hành động của service.
Ngược lại ở class dưới đây:
class UserApplicationService {
private boolean sendMail;
register() {
if (sendMail) {
sendEmail();
}
}
}
Biến sendMail
có ảnh hưởng trực tiếp đến việc gửi email hay không của register service.
Việc có "trạng thái" này sẽ làm cho việc sử dụng service sẽ trở nên phức tạp hơn khi cần phải quan tâm đến "trạng thái" để biết được cách gọi hàm, sử dụng service sao cho phù hợp.
Ngoài ưu điểm như đã nói, Service Locator cũng có những nhược điểm như sau:
class UserApplicationService {
constructor();
register();
}
Đây chính là UserApplicationService
khi nhìn từ bên ngoài do ta đã sử dụng Service Locator
để giải quyết vấn đề phụ thuộc giữa instance với môi trường. Nếu như vậy, khi đọc code ta chỉ có thể hiểu rằng instance được tạo từ class này sẽ chỉ có method register, thế nhưng có thể việc thực thi method register cũng có thể gặp lỗi vì ta cần đăng kí với ServiceLocator về instance sẽ trả về (UserRepository hay InMemoryUserRepository) -> Nếu không đọc kĩ code thì sẽ không thể hiểu nguyên nhân gây ra lỗi.
ServiceLocator
khi test ta cần đăng kí như sau:ServiceLocator.Register<IUserRepository, InMemoryUserRepository>();
const userApplicationService = UserApplicationService();
Nếu ta thêm vào IFooRepository
class UserApplicationService {
private readonly IUserRepository userRepository;
private readonly IFooRepository fooRepository;
constructor() {
this.userRepository = ServiceLocator.resolve<IUserRepository>();
this.fooRepository = ServiceLocator.resolve<IFooRepository>();
}
}
Khi đó nếu chạy test mà không đăng kí thêm IFooRepository
thì sẽ có lỗi xảy ra (nhưng nguyên nhân lại không nằm ở test).
Xem xét việc xây dựng chức năng User cho hệ thống. Với hệ thống thì chức năng User sẽ gồm những chức năng con sau đây:
class User {
private readonly UserName;
private readonly UserId;
}
class UserName {
string value;
}
class UserId {
string id;
}
Ở đây UserId
, UserName
sẽ là các Value-Object class.
Ngoài ra cần phải có Domain Service
để check duplicate user
class CheckDuplicateUserService {
checkDuplicateUser() {}
}
Cuối cùng là Repository để phục vụ cho việc lưu trữ, lấy dữ liệu ra từ storage.
interface IUserRepository {
find();
save();
delete();
}
Với tính năng đăng kí user, ta sẽ tạo ApplicationService như sau:
class UserApplicationService {
private readonly userRepository;
private readonly userService
register(UserName name) {
const user = new User(name);
if (this.userService.checkDuplicateUser(user)) throw new Exception;
this.userRepository.save(user);
}
}
Với tính năng lấy thông tin user, ta sẽ có như sau:
class UserApplicationService {
private readonly userRepository;
// omit register method
find(UserId id) {
const user = this.userRepository.find(id);
return user;
}
}
Ở đây giá trị trả về sẽ là Domain Object
. Vấn đề không nằm ở việc domain object được trả về mà là cách domain object được sử dụng phía client.
class Client {
main() {
const user = UserApplicationService.find(id);
user.changeName(new UserName("newName"));
}
}
Về bản chất, việc sử dụng các phương thức của Domain Object
chỉ được cho phép tại Application Service
. Nếu vượt qua quy tắc này thì các đoạn code có cùng chức năng sẽ bị rải rác ở nhiều chỗ. Ngoài ra còn một vấn đề khác đó là "việc phụ thuộc vào domain object", khi domain rules thay đổi thì domain object cũng sẽ phải thay đổi theo và khi đó chắc chắn sẽ phải chỉnh lại code ở nhiều chỗ.
Để tránh những rủi ro như trên, có một luồng suy nghĩ cho rằng:
Không nên công khai Domain Object
Mà thay vào đó, sẽ sử dụng DTO (Data Transfer Object) để trả về phía client (DTO sẽ chỉ chứa data mà không chứa bất kì một phương thức nào).
class UserDto {
constructor(UserId id, UserName name) {
this.id = id;
this.name = name;
}
}
Khi đó Application Service
sẽ như sau:
class UserApplicationService {
private readonly userRepository;
// omit register method
find(UserId id) {
const user = this.userRepository.find(id);
const userDto = new UserDto(user.id, user.name);
return userDto;
}
}
Ở constructor của UserDto, nếu để các params bị tách lẻ như vậy, khi có sự thay đổi (VD: thêm email property chẳng hạn) thì ngoài việc sửa constructor thì ta cũng cần phải sửa ở tất cả các chỗ có sử dụng UserDto. Vậy nên thay vì để các params bị tách lẻ, ta sẽ túm gọn lại trong một object User (Domain Object).
Việc sử dụng object sẽ phát sinh một vấn đề đó là quan hệ phụ thuộc. Khi các objects phụ thuộc vào nhau quá nhiều, chỉ cần một thay đổi nhỏ ở một object cũng đủ để gây ảnh hưởng lên một phạm vi rộng.
Trên thực tế, rất khó để tránh được quan hệ phụ thuộc này, thế nên thay vì "tránh" nó hãy tìm cách "kiểm soát" nó.
Trong chương trình cũng có sự phân cấp level
Các khái niệm module cấp cao, cấp thấp trong các nguyên tắc của "quan hệ phụ thuộc ngược" cũng được định nghĩa tương tự.
Như ví dụ về UserApplicationService
sử dụng UserRepository
thì UserRepository
là cấp thấp, UserApplicationService
là cấp cao.
Nếu UserApplicationService
sử dụng UserRepository
thay vì interface của nó thì UserApplicationService
sẽ phụ thuộc vào công nghệ sử dụng phía UserRepository
tức là module cấp cao sẽ phụ thuộc vào module cấp thấp. Điều này vi phạm quy tắc của "quan hệ phụ thuộc ngược"
Repository được định nghĩa dưới dạng interface (trừu tượng hoá)
interface IUserRepository {
User find(UserName name);
void save(User user);
}
Ta cũng có thể xem xét đến việc đưa method exist
vào Repository nhưng việc check duplicate này lại gần với domain mà chức năng của Repository chỉ là lưu trữ dữ liệu vào DB nên việc đưa exist
method vào Repository là không hợp lí.
Không nên đưa các xử lí liên quan đến tầng infrastructure vào trong domain service.
Dependency Inversion Principle
Nguyên tắc của quan hệ phụ thuộc ngược:
Repository có chức năng là lưu trữ
và tái cấu trúc
domain object
. Việc lưu trữ này không giới hạn ở Relational Database
mà cũng có thể là:
Ngoài 2 chức năng trên Repository còn đảm bảo là có thể lấy và đưa ra dữ liệu từ nơi lưu trữ.
Với ví dụ về User như ở các chương trước, ta có thể vận dụng repository như sau:
class Program {
private IUserRepository userRepository;
constructor(IUserRepository userRepository) {
this.userRepository = userRepository;
}
void createUser(UserName name) {
const newUser = new User(name);
const userService = new UserService(userRepository);
if (userService.exist(newUser)) throw Exception;
userRepository.save(newUser);
}
}
Việc lưu User object sẽ được uỷ nhiệm lại cho UserRepository
nên việc lưu vào RDB hay NoSQL hay file là không quan trọng đối với domain.
Lúc này Domain Service UserService
sẽ như sau:
class UserService {
private IUserRepository userRepository;
constructor(IUserRepository userRepository) {
this.userRepository = userRepository;
}
exists(User user) {
const found = userRepository.find(user.name);
return found !== null;
}
}
Ta có thể thấy thông qua object trừu tượng là userRepository
mà các thao tác như lưu trữ, sửa đổi, tìm kiếm dữ liệu có thể thực hiện một cách dễ dàng, tách biệt hoàn toàn so với business logic
.
Trong DDD, sau khi tiến hành modeling
ta sẽ thu được các models
các models
này được gọi là domain models
.
Trong thực tế việc lựa chọn value-object phải dựa theo domain model.
Như ví dụ dưới đây, là sự phụ thuộc của ObjectA vào ObjectB
class ObjectA {
private ObjectB objectB;
}
Ngoài ra còn có thể thấy quan hệ phụ thuộc còn xuất hiện giữa class và interface
interface IUserRepository {
find();
}
class UserRepository implements IUserRepository {
find() {}
}
Khi interface không tồn tại thì sẽ gây ra lỗi khi compile code do UserRepository
class triển khai interface IUserRepository
.
Lấy một ví dụ khác như sau:
class UserApplicationService {
private readonly UserRepository userRepository;
register() {
//....
this.userRepository.find();
// ....
}
}
Quan hệ giữa UserApplicationService
và UserRepository
đúng là quan hệ phụ thuộc nhưng nó cũng phụ thuộc luôn về mặt kĩ thuật khi UserApplicationService
sẽ cần phải quan tâm đến loại DB mà UserRepository
sử dụng (RDB hay NoSQL). Nếu sử dụng interface IUserRepository
thì vấn đề phụ thuộc đến hạ tầng kĩ thuật sẽ được giải quyết (có thể thay đổi từ RDB sang NoSQL hoặc ngược lại mà không ảnh hưởng gì đến UserApplicationService
). Từ đó sẽ tách riêng được business logic với phần implementation.
Không sử dụng trực tiếp câu lệnh SQL.
ORM cần một model class để sử dụng như là Entity (model này thường sẽ giống với cấu trúc các bảng của DB).
Model class này sẽ được định nghĩa ở tầng infrastructure
.
Phát triển phần mềm theo cách truyền thống sẽ làm cho các module cấp cao phụ thuộc vào module cấp thấp (yếu tố trừu tượng sẽ phụ thuộc vào yếu tố cụ thể).
Điều này vô tình dẫn đến hệ quả, nếu module cấp thấp có sự thay đổi, nó sẽ tạo ra ảnh hưởng lớn đến các modules cấp cao.
Các domain rules thương nằm ở level cấp cao, nếu level cấp cao phụ thuộc vào level cấp thấp thì khi level cấp thấp thay đổi (thay đổi DB) sẽ gây ảnh hưởng đến domain rules. Đây là một điều rất kì cục và nên tránh.
Module cấp cao nên là TRUNG TÂM của phần mềm thay vì các module cấp thấp
Các modules cấp cao chỉ nên sử dụng các modules cấp thấp với tư cách của một client. Tức là thuần tuý đưa ra các lời gọi/ yêu cầu tới cho modules cấp thấp.
Là thể hiện của Domain Model.
Ta xét ví dụ về UserApplicationService
.
class UserApplicationService {
private readonly IUserRepository userRepository;
constructor() {
userRepository = new InMemoryUserRepository();
}
}
UserRepository
sẽ được dùng cho production còn InMemoryUserRepository
sẽ được dùng cho develop. Nhưng nếu ứng dụng có vấn đề, ngoài việc phải tái hiện lại được bug, ta cần chuẩn bị dữ liệu. Hơn thế nữa phải quay về sử dụng InMemoryUserRepository
- đây là một việc khá phiền phức.
Để giải quyết vấn đề trên, ta sử dụng hai pattern:
Value-Object là object mang giá trị đặc trưng riêng của hệ thống (khác với các giá trị nguyên thuỷ). Khi value-object đứng riêng thì nó không có ý nghĩa gì cả, nó chỉ có ý nghĩa khi đính kèm với một entity.
VD: Với hệ thống quản lí nhân viên thì ID là một Value-Object. Khi ID này đứng riêng, nó hoàn toàn không có ý nghĩa gì, nó chỉ có ý nghĩa khi gắn liền với entity là nhân viên.
Việc sử dụng interface sẽ đảm bảo tính uyển chuyển cho Application Service. Phía client thay vì gọi trực tiếp Application Service thì sẽ gọi thông qua Interface.
Hơn nữa nó còn giúp tiết kiệm thời gian cho dev phía client. Khi Application Service chưa hoàn thiện, thay vì chờ đợi thì phía client có thể tạo ra các class Mock
để mô phỏng lại phía service do phía client chỉ gọi đến interface.
class MockUserRegisterService implements IUserRegisterService {
void handle() {}
}
Do chỉ dừng ở mức mô phỏng service nên phía client hoàn toàn có thể test được cả trường hợp phía service phát sinh ra Exception
class MockUserRegisterService implements IUserRegisterService {
void handle() {
throw new Exception();
}
}
Ưu điểm
Nhược điểm
Lời khuyên ở đây là:
Nếu vẫn còn sự phân vân thì hãy lựa chọn "Object không thay đổi trạng thái".
Entities là các Domain Object được phân biệt bằng identity còn Value-Ọbject sẽ được phân biệt bằng các thuộc tính
VD: Name Value Object sẽ được phân biệt bằng thuộc tính firstName và lastName
Các tính chất của entity:
Khác với value-object, entity có thể thay đổi, ví dụ như "tuổi" và "chiều cao" của một người có thể thay đổi.
Xét ví dụ sau:
class User {
private string name;
constructor(string name) {
this.name = name;
}
}
Hiện tại thì không thể thay đổi "name" của user.
class User {
private string name;
constructor(string name) {
changeName(name);
}
changeName(string name) {
this.name = name;
}
}
Với method changeName
ta hoàn toàn có thể thay đổi giá trị của name. Khác với "value-object" chỉ có thể "thay đổi" giá trị thông qua việc tạo và gán instance mới thì entity hoàn toàn có thể được thay đổi trực tiếp.
Tuy nhiên ta nên hạn chế việc thay đổi trực tiếp thuộc tính của entity, chỉ thay đổi những thuộc tính cần thay đổi.
Với các trường hợp dữ liệu bị lỗi thì ngoài cách throw ra Exception trên server thì nên kiểm tra trước ở phía client để tránh trường hợp server gặp lỗi.
firstName
và lastName
trùng nhau thì sẽ được xem như là một object, còn ở entity thì không hề có chuyện đó. Ta có thể thêm vào class User
ở phía trên identity là UserId
.class UserId {
private string value;
constructor(string value) {
this.value = value;
}
}
class User {
private string name;
private UserId id;
constructor(UserId id, string name) {
this.id = id;
this.name = name;
}
}
Equal
thực hiện phép so sánh các id để xem 2 object có khác nhau hay không.Trong khi value-object cần so sánh mọi thuộc tính để phân biệt 2 object thì entity chỉ cần so sánh identity
là đủ.
Việc sử dụng value-object hay entity là hoàn toàn phụ thuộc vào ngữ cảnh hiện có. Lấy ví dụ:
value-object
sẽ hợp lí hơn.entity
sẽ hợp lí hơn.Entity
và Value-object
. Ta có khái niệm Aggregate root
, từ bên ngoài, các thao tác sẽ chỉ tác động đến root entity này mà thôi. Lấy ví dụ như: Entity "Order", bên trong có "OrderDetail". Khi bên ngoài muốn thao tác (thay đổi, update) lên "Order" thì chỉ có "Order" bị tác động, "OrderDetail" sẽ không bị tác độngRepository: sẽ là nơi thực hiện "Lấy về", "Lưu trữ" các Aggregates, Entities. Trên thực tế, đây sẽ là nơi tương tác trực tiếp với DB, cũng như API bên ngoài. Trong tầng domain, các repositories sẽ được viết dưới dạng interface, phần triển khai sẽ do infrastructure đảm nhận
Domain service: sẽ là nơi viết các logic xử lí không liên quan đến Aggregate, Entity, Value-object. VD: kiểm tra tính unique của user id.
Ref: https://codezine.jp/article/detail/10776
https://qiita.com/tarokamikaze/items/d368e31edefc6a705625
Có 2 ưu điểm cơ bản như sau:
Với các lập trình viên bảo trì hệ thống hoặc tham gia giữa chừng vào dự án thì việc có đầy đủ kiến thức về dự án, hệ thống hiện tại là điều không thể. Dù có bản thiết kế nhưng thiết kế chỉ bao quát được những điều kiện to, còn những điều kiện nhỏ, chi tiết thì chỉ có đọc code mới hiểu được. Ngoài ra có những trường hợp hệ thống vận hành không khớp với thiết kế 100% thì việc đọc code lại trở nên quan trọng hơn bao giờ hết.
Giả sử với class UserName như sau:
class UserName {
public string name { get; set; }
}
Thì việc hiểu ý nghĩa của nó khó hơn rất nhiều so với việc có một class UserName như thế này:
class UserName {
private string name;
constructor(string name) {
if (name.length < 3) throw Exception;
this.name = name;
}
}
Sẽ dễ dàng để người đọc code hiểu được điều kiện cần có của UserName là gì.
Mọi domain rules sẽ được viết vào các Domain Object
. Nếu không thì để hiểu rõ được các domain rules thì chỉ có một cách duy nhất đó là ĐỌC LẠI toàn bộ code hiện có (điều này đối với người có kinh nghiệm cũng không phải một chuyện dễ dàng gì).
Trên thực tế, việc domain rules thay đổi là rất hay xảy ra, nên việc hệ thống có thể phản ánh ngay lập tức sự thay đổi đó là một điều rất tuyệt vời.
Chức năng thay đổi thông tin người dùng có thể thay đổi theo thời gian khi thêm, hoặc bớt các thuộc tính muốn thay đổi, để tránh việc phải thay đổi code ở nhiều chỗ, ta có thể sử dụng Command Object
class UserUpdateCommand {
constructor(id) {
this.id = id;
}
// idGetter
// nameGetter, nameSetter
// emailGetter, emailSetter
}
class UserUpdateCommand {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
// idGetter
// nameGetter
// emailGetter
}
Method update của ApplicationService sẽ như sau:
class UserApplicationService {
update(UserCommand command) {
const user = userRepository.find(new UserId(command.id));
if (command.name) {
user.changeName(new UserName(command.name);
}
if (command.email) {
user.changeEmail(new UserEmail(command.email));
}
userRepository.save(user);
}
}
Tránh việc viết quá nhiều các phương thức update
ví dụ như sau:
class UserRepository implements IUserRepository {
void updateName (UserName name) {}
void updateGender (UserGender gender) {}
void updateEmail (UserEmail email) {}
}
Thay vào đó chỉ sử dụng một phương thức update
duy nhất. Ngoài ra ta không nên định nghĩa phương thức create object
trong Repository
, công việc này sẽ do Factory
đảm nhận.
Theo như vòng đời của object, ta cần phương thức để huỷ object khi không cần thiết nữa. Việc này cũng nên được thực hiện trong Repository
.
Phương thức liên quan đến truy vấn dữ liệu hay sử dụng nhất là tìm kiếm dữ liệu bằng ID
class UserRepository implements IUserRepository {
User find (UserID id) {}
}
Tuy nhiên cũng có những trường hợp cần đến phương thức findAll
để lấy ra toàn bộ dữ liệu có trong DB. Nhưng nếu xét đến hiệu năng thì phương thức này có hiệu năng khá tồi nên hãy chú trọng việc định nghĩa các phương thức tìm kiếm theo điều kiện.
class UserRepository implements IUserRepository {
User findById (UserID id) {}
User findByName (UserName name) {}
}
Với việc sử dụng repository thì business logic hoàn toàn không phụ thuộc vào kĩ thuật sử dụng để lấy, lưu dữ liệu ở tầng infrastructure
.
Có thể trong usecase hoặc domain-service sử dụng IRepository
nhưng thực chất các lời gọi hàm này đều được chuyển tiếp đến class Repository
.
Trong thực tế, từ service
được dùng với nhiều ý nghĩa khác nhau. Nhưng trong phát triển phần mềm, từ service
có thể hiểu theo ý nghĩa là "thứ được tạo ra để phục vụ cho người dùng".
Trong DDD thì service
được chia thành 2 nhóm
Là object thực hiện việc trừu tượng hoá các xử lí như "lưu dữ liệu" hoặc "chỉnh sửa dữ liệu" trong database.
Trong thực tế, chúng ta sẽ không trực tiếp lưu dữ liệu vào trong DB mà sẽ thông qua Instance của Repository
.
Repository khác so với Domain Object mà chúng ta đã từng biết. Repository có chức năng làm nổi bật vai trò của Domain Object
đối với Domain Model
.
Việc so sánh thông qua equal method sẽ có ưu điểm là khi thêm các thuộc tính mới ta không cần thiết phải sửa lại code.
Ví dụ với Object FullName như sau:
// Ban đầu chỉ có firstName và lastName
var result = nameA.firstName === nameB.firstName && nameA.lastName === nameB.lastName;
// Nếu có thêm middleName
var result = nameA.firstName === nameB.firstName
&& nameA.lastName === nameB.lastName
&& nameA.middleName === nameB.middleName;
Việc thay đổi này tuy dễ dàng nhưng nếu nó được lặp đi lặp lại nhiều lần cũng sẽ khiến cho dev cảm thấy mệt mỏi và có thể phát sinh nhầm lẫn.
Nếu sử dụng equal method
ta chỉ cần thay đổi equal method
là đủ.
boolean equal(other) {
return string.Equals(firstName, other.firstName)
&& string.Equals(lastName, other.lastName)
&& string.Equals(middleName, other.middleName);
}
Không chỉ equal method
với các trường hợp khác ta cũng chỉ cần sửa ở một chỗ là đủ.
Giá trị là không thể thay đổi nhưng nếu không thay đổi giá trị thì không thể phát triển được một phần mềm như mong muốn.
Ví dụ:
var c = 1;
c = 2;
Thay vì hiểu theo hướng thay đổi giá trị, hãy nghĩ rằng ta đang thay thế giá trị của biến c
. Tương tự vậy với value-object, ta không thể thay đổi giá trị của nó nhưng có thể thay thế giá trị cho nó.
var fullName = new FullName("ABC", "XYZ")
fullName = new FullName("ABC", "DEF")
Là quá trình trừu tượng hoá các đối tượng trong thực tế. Tuỳ vào domain (lĩnh vực) mà việc mô hình hoá sẽ có sự khác biệt.
VD: Cùng là một cây BÚT nhưng với:
Trong hệ thống sẽ có các phương thức không phù hợp để đưa vào value-object
hoặc entity
. Những phương thức như vậy sẽ được đưa vào Domain Service
.
class User {
private readonly UserName userName;
constructor(UserName name) {
this.userName = name;
}
exists(User user) {
// check user exists or not
}
}
Khi đó nếu kiểm tra user có tồn tại hay không ta sẽ gọi như sau:
user.exists(otherUser)
Biến user
tự gọi đến hàm exists
thì trông không hợp lí một chút nào cả. Vậy nên ta sẽ đưa nó vào trong Domain Service
.
Về bản chất thì Domain Service
cũng là một object. Domain Service
cho User sẽ viết như sau:
class UserService {
public boolean Exists(User user) {}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.