Copy Link

初探Domain-Driven Design - 領域驅動設計

Domain-Driven Design,簡稱DDD,領域驅動設計,是一種程式架構思想和方法。DDD强調將業務邏輯和數據放置在領域中,並通過代碼結構反映出這個領域,它强調“做什麽”。

  1. DDD是一個實踐
  2. 代碼的核心就是業務

DDD的核心哲學

戰術設計

DDD的適用範圍

DDD適合複雜且非簡單的CRUD的項目,因爲:

并非所有項目都應該采用DDD。

DDD最經典的項目結構

Project
└── src
    ├── UI
    ├── Application
    ├── Domain
    │   ├── Aggregates
    │   ├── Events
    │   ├── Primitives
    │   ├── Repositories
    │   ├── ValueObjects
    │   └── Exceptions
    └── Infrastructure

DDD的模式

Primitives

模型的基礎信息,通常是一些接口和抽象類。

public abstract class Entity {
    public int Id { get; protected set; }
}

Entity

Entity,實體,一個具有唯一標識符,且有連續生命周期(更改一些屬性后仍是同一個實體,例如儅一個人長大一歲后,這個人還是同一個人)。

有哪些實體?

Aggregates

Aggregate表示聚合,DDD的核心。Aggregates用於存放聚合根和聚合的内部實體。

如何判斷一個實體是聚合還是聚合根?

  1. 是否可以獨立存在
  1. 是否是業務創造的主要入口

如下結構中,Student是聚合根,Course是聚合根,而Enrollment是一個聚合,Score是一個實體或者是值對象(既不是聚合也不是聚合根)。通過CourseId和StudentId就可以在Enrollment中表達選課邏輯,通過Student對外公開選課動作。

/Aggregates
  ├── Student.cs
  ├── Enrollment.cs
  ├── Score.cs
  └── Course.cs
public class Enrollment {
    public Guid Id { get; private set; }

    public Guid StudentId { get; private set; }
    // Course 是另一個聚合,這裡只保存其Id
    public Guid CourseId { get; private set; }
    // ...
}
public class Student {
    public Guid Id { get; private set; }

    // 聚合對象的列表
    private readonly List<Enrollment> _enrollments = new List<Enrollment>();
    // 對外公開的聚合對象的只讀列表
    public IReadOnlyCollection<Enrollment> Enrollments => _enrollments.AsReadOnly();

ValueObjects

ValueObject表示值對象,通常用來存放地址、性別、年齡等表示值得對象。

public sealed class Gender : IEquatable<Gender> {

    public string Name { get; }
    public string Code { get; }

    private Gender(string name, string code) {
        this.Name = name;
        this.Code = code;
    }

    public static readonly Gender Male = new("Male", "M");
    public static readonly Gender Female = new("Female", "F");
    public static readonly Gender Other = new("Other", "O");

    private static IReadOnlyCollection<Gender> GetAllGenders() => [Male, Female, Other];

    public static Result<Gender> FromCode(string code) {
        if (string.IsNullOrWhiteSpace(code)) {
            return Result.Failure<Gender>("Gender code cannot be empty.");
        }
        var gender = GetAllGenders().FirstOrDefault(gender => gender.Code.Equals(code, StringComparison.OrdinalIgnoreCase));
        return gender is not null ? Result.Success(gender) : Result.Failure<Gender>($"Invalid gender code: '{code}'.");
    }

    public override bool Equals(object? obj) => obj is Gender other && Equals(other);
    public bool Equals(Gender? other) => other is not null && this.Code == other.Code;
    public override int GetHashCode() => this.Code.GetHashCode();
    public static bool operator ==(Gender left, Gender right) => left.Equals(right);
    public static bool operator !=(Gender left, Gender right) => !left.Equals(right);
    public override string ToString() => this.Name;

}

Repositories

Repository表示存儲模式的意圖。通常聲明數據的創建、查詢、更新、刪除等操作。Repositories不會在DDD中實現。

public interface IStudentRepository {

    Task AddOneAsync(Student student, CancellationToken cancellationToken = default);

    Task<Student?> FindOneByIdAsync(Guid id, CancellationToken cancellationToken = default);

    Task<IEnumerable<Student>> FindAllAsync(CancellationToken cancellationToken = default);

    Task<Student?> RemoveOneByIdAsync(Guid id, CancellationToken cancellationToken);

}

Events

事件捕獲領域中已經發生的重要事實(例如 OrderPlaced、PaymentConfirmed)。領域事件是實現限界上下文之間解耦、觸發副作用(如發送郵件)以及實現最終一致性的關鍵。