Chinh Phục Logic Nghiệp Vụ Phức Tạp Với Domain Model


Tổng quan

Phần mềm phức tạp thường bắt đầu rất đơn giản. Nhưng theo thời gian, với vô số yêu cầu mới và tính năng được thêm vào, mã nguồn dần trở nên cồng kềnh, rối rắm và khó bảo trì. Đặc biệt, những dự án có business logic phức tạp thường đối mặt với thách thức này sớm hơn các dự án khác.

Nguyên nhân chính dẫn đến tình trạng này? Đó chính là cách chúng ta tổ chức và triển khai logic nghiệp vụ trong hệ thống. Khi logic nghiệp vụ bị phân tán khắp ứng dụng, việc thay đổi hay mở rộng trở nên vô cùng khó khăn.

Bài viết này sẽ giới thiệu cách tiếp cận Domain-Driven Design (DDD) để giải quyết triệt để vấn đề trên. Thay vì tập trung vào các khía cạnh kỹ thuật, DDD đặt trọng tâm vào việc mô hình hóa miền nghiệp vụ (domain) – nơi chứa đựng những quy tắc và quy trình phức tạp của tổ chức.

Domain Model – Giải pháp cho logic nghiệp vụ phức tạp

Domain Model là một trong những mẫu thiết kế trọng tâm của DDD, giúp ứng dụng của bạn không chỉ phản ánh đúng yêu cầu nghiệp vụ hiện tại mà còn dễ dàng thích ứng với những thay đổi trong tương lai. Với Domain Model, logic nghiệp vụ được tổ chức thành các đối tượng miền rõ ràng, liên kết chặt chẽ với các khái niệm thực tế trong lĩnh vực của bạn.

Những ưu điểm nổi bật của phương pháp này:

  • Logic nghiệp vụ được tập trung và tổ chức có cấu trúc
  • Dễ dàng nhận biết và bảo trì các quy tắc nghiệp vụ
  • Mô hình linh hoạt, dễ dàng thích ứng với thay đổi
  • Cải thiện khả năng giao tiếp giữa các bên liên quan

Hãy cùng khám phá Domain Model thông qua một ví dụ cụ thể về hệ thống quản lý quy trình làm việc, nơi chúng ta sẽ thấy cách áp dụng các khái niệm này vào thực tế.

Logic phức tạp và Active Record: Khi nào không còn đủ?

Khi bắt đầu phát triển một ứng dụng, mẫu thiết kế Active Record thường là lựa chọn tự nhiên và hiệu quả. Đây là cách tiếp cận cho phép một đối tượng đóng gói cả dữ liệu và hành vi, đồng thời xử lý việc lưu trữ trong cơ sở dữ liệu.

Với các ứng dụng CRUD đơn giản, Active Record hoạt động rất tốt:

  • Dễ hiểu và triển khai nhanh chóng
  • Tương thích tốt với các framework phổ biến
  • Hiệu quả cho các tác vụ cơ bản

Tuy nhiên, khi logic nghiệp vụ trở nên phức tạp, Active Record bắt đầu bộc lộ hạn chế. Xét ví dụ về hệ thống quản lý quy trình làm việc (workflow management):

  • Mỗi quy trình có nhiều bước (steps), với các điều kiện chuyển đổi phức tạp
  • Mỗi người dùng có những quyền hạn khác nhau tại từng bước
  • Có các quy định về thời gian hoàn thành mỗi bước (SLA)
  • Hệ thống phê duyệt đa cấp với nhiều quy tắc và điều kiện

Khi dùng Active Record, code nhanh chóng trở nên cồng kềnh:

class WorkflowItem {
  id: number;
  title: string;
  status: string;
  assigneeId: number;
  createdAt: Date;
  dueDate: Date;
  priority: number;
  // ...và hàng chục thuộc tính khác
  
  moveToNextStep(userId: number, nextStepId: string): boolean {
    // Kiểm tra quyền của người dùng
    const user = UserTable.findById(userId);
    if (!user.hasPermission('move_workflow')) {
      return false;
    }
    
    // Kiểm tra trạng thái hiện tại
    if (this.status !== 'IN_PROGRESS') {
      return false;
    }
    
    // Xác định các bước tiếp theo hợp lệ
    const validNextSteps = WorkflowStepTable.findValidNextSteps(this.currentStepId);
    if (!validNextSteps.includes(nextStepId)) {
      return false;
    }
    
    // Cập nhật trạng thái
    this.previousStepId = this.currentStepId;
    this.currentStepId = nextStepId;
    
    // Tính toán hạn mới dựa trên SLA
    const nextStep = StepDefinitionTable.findById(nextStepId);
    this.dueDate = this.calculateNewDeadline(nextStep);
    
    // Lưu thay đổi
    Database.execute(
      'UPDATE workflow_items SET current_step_id = ?, previous_step_id = ?, due_date = ? WHERE id = ?',
      [nextStepId, this.previousStepId, this.dueDate, this.id]
    );
    
    // Gửi thông báo
    NotificationService.send({
      type: 'STEP_CHANGED',
      workflowId: this.id,
      userId: userId,
      oldStep: this.previousStepId,
      newStep: nextStepId
    });
    
    return true;
  }
  
  // ...và hàng chục phương thức phức tạp khác
}

Những vấn đề chính khi sử dụng Active Record cho logic phức tạp:

  1. God Object – Class trở nên quá lớn với quá nhiều trách nhiệm
  2. Business logic bị xáo trộn với code truy cập database
  3. Khó kiểm thử do phụ thuộc vào database
  4. Dễ phát sinh lỗi khi các quy tắc nghiệp vụ bị phân tán
  5. Khó mở rộng khi có yêu cầu mới

Đây là lúc Domain Model thể hiện sức mạnh của mình.

Domain Model: Trọng tâm là nghiệp vụ, không phải dữ liệu

Domain Model là cách tiếp cận tổ chức logic nghiệp vụ thành một mô hình phản ánh các khái niệm, quy tắc và hành vi trong miền nghiệp vụ. Mô hình này tách biệt rõ ràng giữa logic nghiệp vụ và cơ sở hạ tầng (infrastructure).

Các thành phần chính của Domain Model:

Value Objects: Đối tượng được định nghĩa bởi giá trị

Value Objects là đối tượng không có danh tính riêng, được xác định hoàn toàn bởi các thuộc tính của nó. Đây là nền tảng cho các khái niệm không thay đổi trong hệ thống của bạn.

Trong hệ thống workflow, một số Value Objects điển hình:

  • StepId – Định danh cho một bước trong workflow
  • Priority – Mức độ ưu tiên của công việc
  • TimeWindow – Khung thời gian cho việc hoàn thành một bước

Value Objects giúp chúng ta thoát khỏi “Primitive Obsession” – tình trạng lạm dụng các kiểu dữ liệu nguyên thủy (strings, numbers) để biểu diễn các khái niệm có ý nghĩa trong domain.

Entities: Đối tượng có danh tính rõ ràng

Entities là các đối tượng có danh tính riêng biệt, có thể thay đổi trạng thái theo thời gian. Trong hệ thống workflow, hai Entity chính là WorkflowItemUser.

Mỗi Entity có một định danh duy nhất (ID) – thường được triển khai như một Value Object. Ví dụ:

class WorkflowItem {
  constructor(
    private readonly id: WorkflowItemId,
    private readonly title: string,
    private currentStep: StepDefinition,
    // ...các thuộc tính khác
  ) {}

  // Các phương thức để thay đổi trạng thái
  moveToNextStep(nextStep: StepDefinition, performedBy: UserId): void {
    // Logic chuyển bước được đóng gói ở đây
    // KHÔNG có code truy cập database
  }
}

Aggregates: Bảo vệ tính toàn vẹn dữ liệu

Aggregate là một nhóm các Entity và Value Object được xem như một đơn vị nhất quán duy nhất. Mỗi Aggregate có một “root” (gốc) – một Entity kiểm soát việc truy cập vào tất cả các đối tượng khác trong Aggregate.

Các đặc điểm quan trọng của Aggregate:

  1. Ranh giới giao dịch – Mọi thay đổi trong Aggregate xảy ra trong một giao dịch duy nhất
  2. Tham chiếu bên ngoài – Các Aggregate khác chỉ có thể tham chiếu đến Aggregate Root, không phải các đối tượng bên trong
  3. Bảo vệ invariants – Aggregate Root đảm bảo mọi thay đổi đều tuân theo các quy tắc nghiệp vụ

Trong hệ thống workflow, WorkflowItem là một Aggregate Root:

class WorkflowItem {
  // ... các thuộc tính khác
  private approvals: Approval[] = [];
  private history: WorkflowHistory[] = [];
  
  requestApproval(requestedBy: UserId): void {
    if (this.status !== WorkflowItemStatus.IN_PROGRESS) {
      throw new Error('Cannot request approval unless workflow item is IN_PROGRESS');
    }

    this.status = WorkflowItemStatus.PENDING_APPROVAL;
    this.approvals.push(new Approval(requestedBy, new Date()));
    this.logHistory(requestedBy, 'APPROVAL_REQUESTED');
    
    // Publish domain event
    this.domainEvents.push(new ApprovalRequestedEvent(
      this.id, requestedBy, this.currentStep.stepId
    ));
  }
}

Domain Events: Thông báo khi có điều quan trọng xảy ra

Domain Events là cách để thông báo các phần khác của hệ thống về những thay đổi quan trọng đã xảy ra. Chúng thường được Aggregate phát ra sau khi có thay đổi trạng thái.

class ApprovalRequestedEvent extends DomainEvent {
  constructor(
    public readonly workflowItemId: WorkflowItemId,
    public readonly requestedBy: UserId,
    public readonly step: StepId
  ) {
    super(crypto.randomUUID(), new Date());
  }
}

Domain Events cho phép các thành phần khác của hệ thống (ví dụ: dịch vụ thông báo, hệ thống báo cáo) phản ứng với các thay đổi quan trọng mà không cần kết nối trực tiếp với Domain Model.

Domain Services: Xử lý logic không thuộc về Entity cụ thể nào

Domain Services xử lý các logic nghiệp vụ phức tạp mà không thuộc về bất kỳ Entity nào. Các service này vẫn thuộc về Domain Layer, nhưng không lưu trữ trạng thái.

class WorkflowProgressionService {
  // ...
  
  evaluateSLAStatus(workflowItem: WorkflowItem): {
    status: 'ON_TRACK' | 'AT_RISK' | 'OVERDUE';
    remainingPercentage: number;
  } {
    // Logic đánh giá tình trạng SLA
  }
}

Áp dụng Domain Model vào thực tế

Để triển khai Domain Model trong các ứng dụng thực tế, chúng ta cần giải quyết vấn đề kết nối domain với cơ sở hạ tầng (database, API bên ngoài, v.v.).

Repository Pattern

Repository Pattern là cách phổ biến để tách biệt Domain Model khỏi cơ sở dữ liệu:

interface WorkflowItemRepository {
  findById(id: WorkflowItemId): WorkflowItem | null;
  save(workflowItem: WorkflowItem): void;
}

class PostgresWorkflowItemRepository implements WorkflowItemRepository {
  // Triển khai cụ thể để lưu trữ và truy xuất từ Postgres
}

Application Services

Application Services điều phối các hoạt động giữa Domain Model, Repositories và các dịch vụ cơ sở hạ tầng khác:

class WorkflowService {
  constructor(
    private readonly workflowItemRepo: WorkflowItemRepository,
    private readonly eventDispatcher: DomainEventDispatcher
  ) {}

  moveToNextStep(
    workflowItemId: WorkflowItemId,
    nextStepId: StepId,
    userId: UserId
  ): void {
    // 1. Load aggregate
    const workflowItem = this.workflowItemRepo.findById(workflowItemId);
    if (!workflowItem) {
      throw new Error('Workflow item not found');
    }
    
    // 2. Execute domain logic
    workflowItem.moveToNextStep(nextStepId, userId);
    
    // 3. Save changes
    this.workflowItemRepo.save(workflowItem);
    
    // 4. Dispatch domain events
    for (const event of workflowItem.getUncommittedEvents()) {
      this.eventDispatcher.dispatch(event);
    }
  }
}

Lợi ích của Domain Model trong thực tế

Sau khi áp dụng Domain Model, bạn sẽ nhận được nhiều lợi ích cụ thể:

1. Logic nghiệp vụ rõ ràng và tập trung
Tất cả logic nghiệp vụ được tập trung trong Domain Objects, làm cho code dễ đọc và hiểu hơn. Khi bạn cần hiểu “làm thế nào để phê duyệt một workflow item”, bạn chỉ cần xem phương thức approve() trong class WorkflowItem.

2. Tính nhất quán cao hơn
Tất cả các thay đổi đối với dữ liệu đều phải đi qua Aggregate Root, đảm bảo rằng tất cả các quy tắc nghiệp vụ được áp dụng một cách nhất quán. Không còn tình trạng business rules bị triển khai không đồng bộ ở các nơi khác nhau.

3. Dễ dàng thích ứng với thay đổi
Khi yêu cầu nghiệp vụ thay đổi, bạn chỉ cần cập nhật logic trong domain objects mà không ảnh hưởng đến các lớp khác như UI, database, API, v.v.

4. Kiểm thử dễ dàng hơn
Domain objects là các POJO/POCO không phụ thuộc vào cơ sở hạ tầng, giúp việc viết unit tests trở nên dễ dàng hơn. Bạn có thể kiểm tra business logic mà không cần thiết lập database thật.

5. Giảm thiểu bug và lỗi logic
Với Domain Model, các bug và lỗi logic thường ít hơn vì logic nghiệp vụ được tập trung và rõ ràng. Các quy tắc nghiệp vụ phức tạp được xác định và kiểm tra kỹ lưỡng.

Khi nào nên dùng Domain Model?

Domain Model không phải là giải pháp cho mọi vấn đề. Nó mang lại giá trị cao nhất khi:

  1. Logic nghiệp vụ phức tạp – Nhiều quy tắc nghiệp vụ phụ thuộc lẫn nhau
  2. Các quy tắc thay đổi thường xuyên – Yêu cầu nghiệp vụ liên tục phát triển
  3. Nhiều invariants – Có nhiều điều kiện phải luôn đúng trong hệ thống
  4. Dự án dài hạn – Ứng dụng cần bảo trì và phát triển trong nhiều năm

Đối với các ứng dụng CRUD đơn giản, Active Record vẫn là lựa chọn hợp lý và tiết kiệm thời gian hơn.

Kết luận

Domain Model là một công cụ mạnh mẽ để quản lý logic nghiệp vụ phức tạp trong phát triển phần mềm. Bằng cách tách biệt business logic khỏi infrastructure concerns và tổ chức nó thành các entities, value objects, và aggregates có ý nghĩa, bạn sẽ xây dựng được một hệ thống dễ hiểu, dễ bảo trì và dễ thay đổi.

Mặc dù Domain Model yêu cầu đầu tư thời gian và nỗ lực ban đầu, nhưng lợi ích dài hạn là rất đáng kể, đặc biệt đối với các hệ thống có logic nghiệp vụ phức tạp. Hãy cân nhắc sử dụng Domain Model cho dự án tiếp theo của bạn nếu nó đáp ứng các tiêu chí được đề cập ở trên.