ddd-introduction's Issues
Modeling
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:
- Writer: sẽ quan tâm đến việc có viết được hay không.
- Present: sẽ quan tâm đến hình thức bên ngoài nhiều hơn.
Tạo Repository sử dụng SQL
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
.
Cần phải hiểu rõ nghiệp vụ (Domain), nếu không thì hệ thống sẽ vận hành không như ý
Interface của Application Service
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();
}
}
Service Locator
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.
Entity là gì ?
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)
Value-object có hành động
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);
}
Động cơ sử dụng value-object
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:
- Tăng khả năng hiển thị các giá trị đặc trưng của hệ thống.
Ta lấy ví dụ ở các công ty kinh doanh sản phẩm, họ sẽ có các mã hàng như:
- ItemNumber
- SerialNumber
- LotNumber
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.
- Không để cho các giá trị "lệch chuẩn" tồn tại.
Ta lấy ví dụ vớiuserName
, ở phía người dùng nó chỉ đơn thuần là mộtstring
. Nhưng ở phía hệ thống sẽ có những quy tắc như:
- Tên phải có độ dàng trong khoảng [n, m]
- Tên chỉ chứa các kí tự alphabet và kí tự số
- ...
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;
}
}
- Tránh việc gán nhầm giá trị.
Việc gán giá trị là việc làm thường thấy đối với các lập trình viên, nhưng việc gán nhầm giá trị vẫn thường xuyên xảy ra.
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.
- Tránh việc logic nằm rải rác, không tập trung.
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.
Các tiêu chuẩn đối với Value-Object
Trong thực tế việc lựa chọn value-object phải dựa theo domain model.
Một ví dụ về Domain Service ứng dụng cho hệ thống logistic
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.
- Định nghĩa các hành động của kho hàng
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.
- Định nghĩa Domain Service
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.
Ưu và nhược điểm của việc không thay đổi trạng thái của object
Ưu điểm
- Tránh được những lỗi tiềm tàng vì tránh được việc trạng thái của object bị thay đổi một cách không mong muốn.
- Thuận lợi hơn khi tiến hành xử lí song song, khi đó ta sẽ không phải ghi nhớ quá nhiều về việc thay đổi giá trị của object.
- Tiết kiệm được tài nguyên bộ nhớ, vì ta có thể cache object do object không bị thay đổi trạng thái.
Nhược điểm
- Khi muốn thay đổi trạng thái hoặc giá trị của object ta phải tạo ra một instance mới của object -> khá tồi về mặt performance.
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".
Ưu điểm của việc định nghĩa Domain Object
Có 2 ưu điểm cơ bản như sau:
- Tăng tính document cho code.
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ì).
- Có thể thay đổi code tuỳ theo domain một cách dễ dàng.
Ví dụ nếu domain rules thay đổi từ việc UserName ít nhất có 3 chữ cái -> 6 chữ cái thì lúc này chỉ cần thay đổi ở class UserName là đủ, ngược lại nếu việc kiểm tra điều kiện trên được phân tán ở nhiều chỗ thì việc sửa code sẽ rất khó khăn.
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.
Domain Object
Là thể hiện của Domain Model.
Application Service là gì ?
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.
Xây dựng Usecase
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:
- Đăng kí
- Xác nhận thông tin
- Thay đổi thông tin
- Rời khỏi hệ thống
- Chuẩn bị Domain Object
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).
Repository Interface
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.
Những pattern cơ bản
- value-object
- entity
- domain-service
Những pattern cần biết khi triển khai ứng dụng:
- Repository
- Application service
- Factory
Các pattern khác cần biết:
- Aggregate
- Specification
So sánh dựa theo tính bình đẳng
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ính chất của Entity
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:
- Có thể thay đổi
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.
- Dù cùng thuộc tính nhưng khác nhau
Với ví dụ về tên người, các value-object có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àoclass 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;
}
}
- Được phân biệt bởi identity
Với class User như ở trên, ta có thể viết methodEqual
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à đủ.
Kiểm soát yếu tố trừu tượng
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.
Value có 3 tính chất sau
- Bất biến
- Có thể thay đổi (exchange)
- Tính bình đẳng (Equality)
Model có thể trở thành Entity hay Value Object
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ụ:
- Bánh xe đối với ô tô là thứ hoàn toàn có thể thay thế, vậy nên việc phân biệt giữa các bánh xe là điều không cần thiết, trong tình huống này sử dụng
value-object
sẽ hợp lí hơn. - Bánh xe trong xưởng sản xuất cần một định danh để biết được bánh xe được sản xuất khi nào, thế nên sử dụng
entity
sẽ hợp lí hơn.
Định nghĩa các phương thức cho Repository
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) {}
}
Phân loại pattern
Điều khiển quan hệ phụ thuộc
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:
- Service Locator
- IoC Container
Định nghĩa sơ lược về pattern
- Value-object: Nó sẽ là lớp bao lấy các kiểu giá trị nguyên thuỷ của ngôn ngữ lập trình, chúng bất biến (immutable) và KHÔNG CÓ ID, nếu với các giá trị có sự liên quan chặt chẽ tới nhau thì Value-object có thể có nhiều thuộc tính giá trị trong đó
VD: Với string --> String-Value-Object class, class này sẽ có các phương thức phụ trợ (concat, ....). String-Value-Object sẽ có các thuộc tính {firstName: string, lastName: string} - Entity: có thuộc tính ID để định danh và là mutable
- Aggregate: là tập hợp của
Entity
vàValue-object
. Ta có khái niệmAggregate 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 động
-
Repository: 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ó thể trao đổi giá trị
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")
Nhiệm vụ của Repository
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à:
- Lưu object vào file
- Lưu object vào NoSQL
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
.
Nguyên tắc của quan hệ phụ thuộc ngược
Dependency Inversion Principle
- Các modules cấp cao không nên phụ thuộc vào module cấp thấp. Cả hai loại modules đều nên phụ thuộc vào các yếu tố trừu tượng
- Các yếu tố trừu tượng không nên phụ thuộc vào việc triển khai, việc triển khai nên phụ thuộc vào các yếu tố trừu tượng.
Nguyên tắc của quan hệ phụ thuộc ngược:
- Tăng tình uyển chuyển, mềm dẻo cho phần mềm
- Đảm bảo cho business logic không phụ thuộc vào các yếu tố công nghệ
Tạo usecase thay đổi thông tin người dùng
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);
}
}
So sánh bằng equal method vs So sánh thông qua thuộc tính
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à đủ.
Repository là gì
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
.
Sự liên quan giữa các tiêu chuẩn của entity với vòng đời
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.
Hãy phụ thuộc vào các yếu tố trừu tượng
Trong chương trình cũng có sự phân cấp level
- Level cấp thấp: gần với các yếu tố công nghệ, xử lí mang tính chất cụ thể
- Level cấp cao: gần với người dùng, con người, mang tính trừu tượng cao
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"
Mối quan hệ giữa Domain, Domain Model, Domain Object
Domain service là gì ?
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
.
- Những phương thức không phù hợp
Ta lấy ví dụ với hệ thống có User, Domain rule quy định các User không được phép trùng tên nhau. Nếu vậy thì nơi ta nên định nghĩa phương thức kiểm tra đó là entity.
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
.
- Giải quyết sự không phù hợp
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) {}
}
Thế nào là phụ thuộc
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.
Tính bất biến của value
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Ị"
Khi domain thay đổi thì domain model cũng sẽ phải thay đổi theo
DDD Example
Rò rỉ Domain Rule
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.
Service là gì ?
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.
Domain Model
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
.
Service Locator cũng là anti-pattern
Ngoài ưu điểm như đã nói, Service Locator cũng có những nhược điểm như sau:
- Khá khó thấy được "quan hệ phụ thuộc" khi nhìn từ bên ngoài.
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.
- Khó để duy trì test
Khi sử dụngServiceLocator
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).
Xây dựng Repository sử dụng ORM
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
.
Sử dụng Domain Service
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
- Nhóm 1: service phục vụ cho domain.
- Nhóm 2: service phục vụ cho ứng dụng.
Điều gì sẽ xảy ra khi lạm dụng Domain Service
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ỗ.
Sự phụ thuộc trong kĩ thuật
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ó.
Mức độ gắn kết với Application Service
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:
- Application.User.UserRegisterService
- Application.User.UserDeleteService
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.
Value-Object là gì
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.
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.