初探Domain-Driven Design - 領域驅動設計
Domain-Driven Design,簡稱DDD,領域驅動設計,是一種程式架構思想和方法。DDD强調將業務邏輯和數據放置在領域中,並通過代碼結構反映出這個領域,它强調“做什麽”。
- DDD是一個實踐
- 代碼的核心就是業務
DDD的核心哲學
- 領域與子領域
- 核心領域:複雜業務邏輯
- 子領域:為核心領域提供簡單的邏輯
- 通用子領域:非業務核心
- 上下文邊界:子領域只服務核心領域
戰術設計
DDD的適用範圍
DDD適合複雜且非簡單的CRUD的項目,因爲:
- 大部分業務邏輯處於Domain,在設計Domain時可以直接和需求溝通
- 通過聚合與聚合根來保證業務規則完整性
- Domain包含一系列模式分層
- 清晰的架構邊界
并非所有項目都應該采用DDD。
- DDD將複雜的業務邏輯放置在領域中,開發者需要在開發之前進行領域模型建模
- 投入成本高(陡峭的學習成本,高團隊協作性要求)
- 不適合簡單的CRUD項目
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用於存放聚合根和聚合的内部實體。
- 聚合是一個實體,是業務邏輯的單元,是一個數據修改單元,是一組在業務上關聯在一起的實體和值對象的集合。
- 聚合根是一個實體,是聚合的特定實體,是整個聚合們的外部入口。
如何判斷一個實體是聚合還是聚合根?
- 是否可以獨立存在
- 是:聚合根
- 否:聚合
- 是否是業務創造的主要入口
- 是:聚合根
- 否:聚合
如下結構中,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)。領域事件是實現限界上下文之間解耦、觸發副作用(如發送郵件)以及實現最終一致性的關鍵。