還記得我們前面介紹的 DDD 三種物件規則嗎?不記得可以回去翻翻 Day 06 - Tactical Design 的文章。
我們這篇章要依照上述的規則來在 Common Library 內實作這些規則的介面,好讓所有 Microservices 都可以依照這些準則實作。
Entity 具有唯一的 ID,其主要特徵是 ID 的不可變性,其他屬性則可以隨著業務邏輯而改變。只要兩個實體的 ID 相同,即使其他屬性不同,它們也被視為相同的物件。以下為 Entity 的實作:
namespace Common.Library.Seedwork;
public abstract class Entity<TId> where TId : notnull
{
// 實體內部持有一個 Domain Event List,用來追蹤事件
private readonly List<IDomainEvent> _domainEvents = new();
// ID 是必需且不可變的
public required TId Id { get; init; }
public DateTime CreatedDateTime { get; set; }
public DateTime UpdatedDateTime { get; set; }
// 取得 Domain Event List
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
// 增加 Domain Event
public void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
// 清除所有的 Domain Events
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
Entity 實作重點:
Domain Event 是一種用來通知外部發生了某些業務邏輯變更的機制。使用 MediatR 來處理事件的傳播:
using MediatR;
namespace Common.Library.Seedwork;
// 定義 Domain Event 接口,繼承自 INotification(MediatR 的接口)
public interface IDomainEvent : INotification
{
}
Value Object 與 Entity 不同,它沒有唯一的 ID。它只關注其屬性的值,而不是身份。Value Object 一旦創建後是不可變的,任何改變都應該通過創建新實例來完成。以下是 Value Object 的實作:
namespace Common.Library.Seedwork;
// Value Object基類,強調不可變性
public abstract class ValueObject
{
// 子類別需實作此方法,回傳物件的屬性集合,用於相等性判斷
// 比如,若有 Address Value Object,它的屬性可能包括 City、Street、ZipCode 等
protected abstract IEnumerable<object> GetEqualityComponents();
// 覆寫 Equals 方法來比較兩個 ValueObject 是否相等
// 使用 SequenceEqual 比較兩個物件的屬性集合是否完全一致
public override bool Equals(object? obj)
{
// 若 obj 為 null 或者 obj 的型別與當前物件的型別不同,則視為不相等
if (obj == null || obj.GetType() != GetType())
return false;
// 轉型為 ValueObject 並使用 GetEqualityComponents 來逐一比較屬性
return GetEqualityComponents()
.SequenceEqual(((ValueObject)obj).GetEqualityComponents());
}
// 覆寫 GetHashCode 方法來生成物件的 Hash Code
// 如果兩個物件的所有屬性值完全相同,則它們的 Hash Code 也會相同
// 透過 GetEqualityComponents 的每個屬性生成 Hash Code,並以 XOR 結合
public override int GetHashCode()
{
return GetEqualityComponents()
.Aggregate(0, (hash, component) => hash ^ (component?.GetHashCode() ?? 0));
}
// 定義 == 運算符,用來比較兩個 ValueObject 是否相等
// 使用 ReferenceEquals 來判斷是否為同一個實例,若不是,則調用 Equals 方法進行比較
public static bool operator ==(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) || ReferenceEquals(right, null))
return ReferenceEquals(left, right);
return left.Equals(right);
}
// 定義 != 運算符,當兩個物件不相等時返回 true,否則返回 false
public static bool operator !=(ValueObject left, ValueObject right)
{
return !(left == right);
}
}
Value Object 實作重點:
Equals
方法,確保 Value Object 是不可變的。Aggregate 是一個由多個 Entity 和/或 Value Object 組成的集合,它定義了內部的邊界上下文,所有操作都必須通過 Aggregate Root 來進行。
namespace Common.Library.Seedwork;
// 聚合根標示接口,所有聚合根都應實作此接口
public interface IAggregateRoot
{
}
Aggregate 實作重點:
因為 Aggregate Root 是一個特定的 Entity,負責管理整個 Aggregate 的一致性與完整性。所以我們必些要用一些 Patterns 來保證對所有的 Aggregates 的操作都會是一致且完整的。
故我們需要多實作 Repository 和 Unit of Work 的介面。Repository 負責資料持久化,而 Unit of Work 則負責管理事務的一致性。
namespace Common.Library.Seedwork;
// 定義儲存庫接口,限制只適用於聚合根
public interface IRepository<T> where T : IAggregateRoot
{
IUnitOfWork UnitOfWork { get; }
}
// 定義 Unit of Work 接口,用來管理事務操作
public interface IUnitOfWork : IDisposable
{
Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default);
}
實作重點:
這裡展示了 DDD 相關核心概念在實作上的具體方式,特別針對 Entity、Value Object 以及 Aggregate 的處理。在 Common Library 設定好清晰的物件規則,我們可以保持系統的模組化,並促進良好的架構設計。