Giter VIP home page Giter VIP logo

ddd-introduction's Introduction

Hello World

by: Vietnamese

I'm Anh, from 🇻🇳 and be a 👨‍💻, like to build a system by DDD.

Currently have interested in System Design 👷 🏗️ and Database.

Github statistic

GitHub stats

Tech Toolbox 🧰

Database 🗃️

MySQL MongoDB

Infrastructure

AWS

Programming Language 💻

JS NodeJS TS Dart Python

UI 🎨

React Redux MUI

Mobile 📱

Flutter

ORM ⚙️

Sequelize

Tool ⚒️

Git

Testing 🧪

Jest

Deep Learning 🤖

Tensorflow

Architecture 🏗️

Clean architeture + DDD

CQRS, Event Sourcing Screen Shot 2023-09-16 at 13 32 19

ddd-introduction's People

Contributors

tuananhhedspibk avatar

Stargazers

 avatar  avatar

Watchers

 avatar

ddd-introduction's Issues

Độ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:

  1. 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.

  1. Không để cho các giá trị "lệch chuẩn" tồn tại.
    Ta lấy ví dụ với 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ư:
  • 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;
  }
}
  1. 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
  }
}

UserNameUserId 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.

  1. 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.

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

Screen Shot 2021-09-25 at 21 14 54

Thử mô hình hoá quá trình vận chuyển này bằng code.

  1. Đị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.

  1. Đị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:

  1. DomainName
    VD: User

  2. 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)

  3. DomainName + DomainService
    VD: UserDomainService.

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".

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à đủ.

Screen Shot 2021-10-23 at 17 27 33

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.

Screen Shot 2021-10-23 at 17 29 17

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-objectEntity 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)

Phân loại pattern

Pattern sẽ được phân thành 2 nhóm:

  • Biểu thị nghiệp vụ (Domain knowledge)
  • Triển khai ứng dụng

Dưới đây là sơ đồ mối liên hệ giữa các pattern:

Screen Shot 2021-07-03 at 21 37 25

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);
}

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.

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.

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

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Ị"

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ả userRepositoryuserService 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 UserRegisterServiceUserDeleteService

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

Screen Shot 2021-10-04 at 8 28 07

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.

Đ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 settergetter. 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ệuPhươ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ỗ.

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.

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ụng 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).

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
  1. 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).

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ó.

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"

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.

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ệ

Nhiệm vụ của Repository

Repository có chức năng là lưu trữ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.

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.

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 UserApplicationServiceUserRepository đú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.

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.

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.

Đ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

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.

Screen Shot 2021-07-04 at 12 23 06

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();
  }
}

Screen Shot 2021-10-10 at 20 45 40

Ưu và nhược điểm của việc không thay đổi trạng thái của object

Ưu điểm

  1. 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.
  2. 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.
  3. 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

  1. 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".

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 firstNamelastName

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ó firstNamelastName 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 identityUserId.
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 method 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à đủ.

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 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 EntityValue-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 động

10776_001_s

  • 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

Ư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.

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);
  }
}

Đị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) {}
}

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.

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.

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.

Screen Shot 2021-09-26 at 15 16 23

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.

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à đủ.

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")

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.

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.

  1. 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.

  1. 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) {}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.