領域驅動設計-實體與聚合根

實體是DDD(Domain Driven Design)中核心概念.Eric Evans是這樣描述實體的 "一個沒有從其屬性,而是通過連續性和身份的線索來定義的對象"

實體通常映射到關係型數據庫的表中。

實體類

實體都繼承自Entity<TKey>類,如下所示:

public class Book : Entity<Guid>
{
    public string Name { get; set; }

    public float Price { get; set; }
}

如果你不想繼承基類Entity<TKey>,也可以直接實現IEntity<TKey>接口

Entity<TKey>類只是用給定的主 鍵類型 定義了一個Id屬性,在上面的示例中是Guid類型.可以是其他類型如stringintlong或其他你需要的類型。

Guid主鍵的實體

如果你的實體Id類型爲 Guid,有一些好的實踐可以實現:

  • 創建一個構造函數,獲取ID作爲參數傳遞給基類.
    • 如果沒有爲GUID Id斌值,ABP框架會在保存時設置它,但是在將實體保存到數據庫之前最好在實體上有一個有效的Id.
  • 如果使用帶參數的構造函數創建實體,那麼還要創建一個 protected 構造函數. 當數據庫提供程序從數據庫讀取你的實體時(反序列化時)將使用它.
  • 不要使用 Guid.NewGuid() 來設置Id! 在創建實體的代碼中使用IGuidGenerator服務傳遞Id參數. IGuidGenerator經過優化可以產生連續的GUID.這對於關係數據庫中的聚集索引非常重要.

示例實體:

public class Book : Entity<Guid>
{
    public string Name { get; set; }
    public float Price { get; set; }
    protected Book()
    {
    }
    public Book(Guid id)
     : base(id)
    {
    }
}

應用服務中使用示例:

public class BookAppService : ApplicationService, IBookAppService
{
    private readonly IRepository<Book> _bookRepository;
    public BookAppService(IRepository<Book> bookRepository)
    {
        _bookRepository = bookRepository;
    }
    public async Task CreateAsync(CreateBookDto input)
    {
        await _bookRepository.InsertAsync(
            new Book(GuidGenerator.Create())
            {
                Name = input.Name,
                Price = input.Price
            }
        );
    }
}
  • BookAppService 注入圖書實體的默認倉庫,使用InsertAsync方法插入 Book 到數據庫中。
  • GuidGenerator類型是 IGuidGenerator,它是在ApplicationService基類中定義的屬性. ABP將這樣常用屬性預注入,所以不需要手動注入
  • 如果您想遵循DDD最佳實踐,請參閱下面的聚合示例部分。

具有複合鍵的實體

有些實體可能需要 複合鍵。在這種情況下,可以從非泛型Entity類派生實體.如:

public class UserRole : Entity
{
    public Guid UserId { get; set; }

    public Guid RoleId { get; set; }
    
    public DateTime CreationTime { get; set; }

    public UserRole()
    {
            
    }
    
    public override object[] GetKeys()
    {
        return new object[] { UserId, RoleId };
    }
}

上面的例子中,複合鍵由UserIdRoleId組成.在關係數據庫中,它是相關表的複合主鍵. 具有複合鍵的實體應當實現上面代碼中所示的 GetKeys()方法。

你還需要在對象關係映射(ORM)中配置實體的鍵。參閱Entity Framework Core集成文檔查看示例。

需要注意,複合主鍵實體不可以使用 IRepository<TEntity, TKey> 接口,因爲它需要一個唯一的Id屬性. 但你可以使用 IRepository<TEntity>。更多信息請參見倉儲的文檔。

聚合根

"聚合是域驅動設計中的一種模式.DDD的聚合是一組可以作爲一個單元處理的域對象。例如,訂單及訂單系列的商品,這些是獨立的對象,但將訂單(連同訂單系列的商品)視爲一個聚合通常是很有用的"( 查看詳細介紹)

AggregateRoot<TKey>類繼承自Entity<TKey>類,所以默認有Id這個屬性

值得注意的是 ABP 會默認爲聚合根創建倉儲,當然ABP也可以爲所有的實體創建倉儲。詳情參見倉儲

ABP不強制你使用聚合根,實際上你可以使用上面定義的Entity類,當然,如果你想實現領域驅動設計並且創建聚合根,這裏有一些最佳實踐僅供參考:

  • 聚合根需要維護自身的完整性,所有的實體也是這樣.但是聚合根也要維護子實體的完整性.所以,聚合根必須一直有效。
  • 使用Id引用聚合根,而不使用導航屬性。
  • 聚合根被視爲一個單元.它是作爲一個單元檢索和更新的.它通常被認爲是一個交易邊界。
  • 不單獨修改聚合根中的子實體。

如果你想在應用程序中實現DDD,請參閱實體設計最佳實踐指南

聚合根例子

這是一個具有子實體集合的聚合根例子:

public class Order : AggregateRoot<Guid>
{
    public virtual string ReferenceNo { get; protected set; }

    public virtual int TotalItemCount { get; protected set; }

    public virtual DateTime CreationTime { get; protected set; }

    public virtual List<OrderLine> OrderLines { get; protected set; }

    protected Order()
    {

    }

    public Order(Guid id, string referenceNo)
    {
        Check.NotNull(referenceNo, nameof(referenceNo));
        
        Id = id;
        ReferenceNo = referenceNo;
        
        OrderLines = new List<OrderLine>();
    }

    public void AddProduct(Guid productId, int count)
    {
        if (count <= 0)
        {
            throw new ArgumentException(
                "You can not add zero or negative count of products!",
                nameof(count)
            );
        }

        var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);

        if (existingLine == null)
        {
            OrderLines.Add(new OrderLine(this.Id, productId, count));
        }
        else
        {
            existingLine.ChangeCount(existingLine.Count + count);
        }

        TotalItemCount += count;
    }
}

public class OrderLine : Entity
{
    public virtual Guid OrderId { get; protected set; }

    public virtual Guid ProductId { get; protected set; }

    public virtual int Count { get; protected set; }

    protected OrderLine()
    {

    }

    internal OrderLine(Guid orderId, Guid productId, int count)
    {
        OrderId = orderId;
        ProductId = productId;
        Count = count;
    }

    internal void ChangeCount(int newCount)
    {
        Count = newCount;
    }

    public override object[] GetKeys()
    {
        return new Object[] {OrderId, ProductId};
    }
}

如果你不想你的聚合根繼承AggregateRoot<TKey>類,你可以直接實現IAggregateRoot<TKey>接口

Order是一個具有Guid類型Id屬性的 聚合根.它有一個OrderLine實體集合.OrderLine是一個具有組合鍵(OrderLine和 ProductId)的實體。

雖然這個示例可能無法實現聚合根的所有最佳實踐,但它仍然遵循良好的實踐:

  • Order有一個公共的構造函數,它需要 minimal requirements 來構造一個"訂單"實例.因此,在沒有IdreferenceNo的時候是無法創建訂單的.protected/private的構造函數只有從數據庫讀取對象時 反序列化 才需要。
  • OrderLine的構造函數是internal的,所以它只能由領域層來創建.在Order.AddProduct這個方法的內部被使用。
  • Order.AddProduct實現了業務規則將商品添加到訂單中。
  • 所有屬性都有protected的set.這是爲了防止實體在實體外部任意改變.因此,在沒有向訂單中添加新產品的情況下設置 TotalItemCount將是危險的.它的值由AddProduct方法維護。

ABP框架不強制你應用任何DDD規則或模式.但是,當你準備應用的DDD規則或模式時候,ABP會讓這變的可能而且更簡單.文檔同樣遵循這個原則。

帶有組合鍵的聚合根

雖然這種聚合根並不常見(也不建議使用),但實際上可以按照與上面提到的跟實體相同的方式定義複合鍵.在這種情況下,要使用非泛型的AggregateRoot基類。

基類和接口的審計屬性

有一些屬性,像CreationTimeCreatorIdLastModificationTime...在所有應用中都很常見. ABP框架提供了一些接口和基類來標準化這些屬性,並自動設置它們的值。

審計接口

有很多的審計接口,你可以選擇一個你需要的來實現它。

雖然可以手動實現這些接口,但是可以使用下一節中定義的基類簡化代碼。

  • IHasCreationTime 定義了以下屬性:
    • CreationTime
  • IMayHaveCreator 定義了以下屬性:
    • CreatorId
  • ICreationAuditedObject 繼承 IHasCreationTime 和 IMayHaveCreator, 所以它定義了以下屬性:
    • CreationTime
    • CreatorId
  • IHasModificationTime 定義了以下屬性:
    • LastModificationTime
  • IModificationAuditedObject 擴展 IHasModificationTime 並添加了 LastModifierId 屬性. 所以它定義了以下屬性:
    • LastModificationTime
    • LastModifierId
  • IAuditedObject 擴展 ICreationAuditedObject 和 IModificationAuditedObject, 所以它定義了以下屬性:
    • CreationTime
    • CreatorId
    • LastModificationTime
    • LastModifierId
  • ISoftDelete (參閱 數據過濾文檔) 定義了以下屬性:
    • IsDeleted
  • IHasDeletionTime 擴展 ISoftDelete 並添加了 DeletionTime 屬性. 所以它定義了以下屬性:
    • IsDeleted
    • DeletionTime
  • IDeletionAuditedObject 擴展 IHasDeletionTime 並添加了 DeleterId 屬性. 所以它定義了以下屬性:
    • IsDeleted
    • DeletionTime
    • DeleterId
  • IFullAuditedObject 繼承 IAuditedObject 和 IDeletionAuditedObject, 所以它定義了以下屬性:
    • CreationTime
    • CreatorId
    • LastModificationTime
    • LastModifierId
    • IsDeleted
    • DeletionTime
    • DeleterId

當你實現了任意接口,或者從下一節定義的類派生,ABP框架就會儘可能地自動管理這些屬性。

實現 ISoftDelete , IDeletionAuditedObject 或 IFullAuditedObject 讓你的實體軟刪除。參閱數據過濾文檔,瞭解軟刪除模式。

審計基類

雖然可以手動實現以上定義的任何接口,但建議從這裏定義的基類繼承:

  • CreationAuditedEntity<TKey> 和 CreationAuditedAggregateRoot<TKey> 實現了 ICreationAuditedObject 接口.
  • AuditedEntity<TKey> 和 AuditedAggregateRoot<TKey> 實現了 IAuditedObject 接口.
  • FullAuditedEntity<TKey> and FullAuditedAggregateRoot<TKey> 實現了 IFullAuditedObject 接口.

所有這些基類都有非泛型版本,可以使用 AuditedEntity 和 FullAuditedAggregateRoot 來支持複合主鍵;

所有這些基類也有 ... WithUser,像 FullAuditedAggregateRootWithUser<TUser> 和 FullAuditedAggregateRootWithUser<TKey, TUser>。這樣就可以將導航屬性添加到你的用戶實體. 但在聚合根之間添加導航屬性不是一個好做法,所以這種用法是不建議的(除非你使用EF Core之類的ORM可以很好地支持這種情況,並且你真的需要它. 請記住這種方法不適用於NoSQL數據庫(如MongoDB),你必須真正實現聚合模式)。

額外的屬性

ABP定義了 IHasExtraProperties 接口,可以由實體實現,以便能夠動態地設置和獲取的實體屬性. AggregateRoot 基類已經實現了 IHasExtraProperties 接口. 如果你從這個類(或者上面定義的一個相關審計類)派生,那麼你可以直接使用API​。

GetProperty 和 SetProperty 擴展方法

這些擴展方法是獲取和設置實體數據的推薦方法。例:

public class ExtraPropertiesDemoService : ITransientDependency
{
    private readonly IIdentityUserRepository _identityUserRepository;

    public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository)
    {
        _identityUserRepository = identityUserRepository;
    }

    public async Task SetTitle(Guid userId, string title)
    {
        var user = await _identityUserRepository.GetAsync(userId);

        //SET A PROPERTY
        user.SetProperty("Title", title);

        await _identityUserRepository.UpdateAsync(user);
    }

    public async Task<string> GetTitle(Guid userId)
    {
        var user = await _identityUserRepository.GetAsync(userId);

        //GET A PROPERTY
        return user.GetProperty<string>("Title");
    }
}
  • 屬性的值是object,可以是任何類型的對象(string,int,bool...等)。
  • 如果給定的屬性未設置值, GetProperty 方法會返回 null。
  • 你可以使用不同的屬性名稱(如這裏的Title)同時存儲多個屬性。

最好爲屬性名定義一個常量防止拼寫錯誤。最佳方式是定義擴展方法來利用智能感知。例:

public static class IdentityUserExtensions
{
    private const string TitlePropertyName = "Title";

    public static void SetTitle(this IdentityUser user, string title)
    {
        user.SetProperty(TitlePropertyName, title);
    }

    public static string GetTitle(this IdentityUser user)
    {
        return user.GetProperty<string>(TitlePropertyName);
    }
}

然後你可以直接使用 IdentityUser 對象的 user.SetTitle("...") 和 user.GetTitle()。

HasProperty 和 RemoveProperty 擴展方法

  • HasProperty 用於檢查對象是否設置了屬性。
  • RemoveProperty 用於從對象中刪除屬性. 你可以使用它來替代設置 null 值。

它是如何實現的?

IHasExtraProperties 要求實現類定義一個名稱爲 ExtraProperties 的Dictionary<string, object> 屬性。

所以,如果你需要你可以直接使用 ExtraProperties 屬性來使用字典API,但是推薦使用 SetProperty 和 GetProperty 方法,因爲它們會檢查 null 值。

它是如何存儲的?

存儲字典的方式取決於你使用的數據庫提供程序.

  • 對於 Entity Framework Core,這是兩種類型的配置;
    • 默認它以 JSON 字符串形式存儲在 ExtraProperties 字段中. 序列化到 JSON 和反序列化到 JSON 由ABP使用EF Core的值轉換系統自動完成.
    • 如果需要,你可以使用 ObjectExtensionManager 爲所需的額外屬性定義一個單獨的數據庫字段. 那些使用 ObjectExtensionManager 配置的屬性繼續使用單個 JSON 字段. 當你使用預構建的應用模塊並且想要擴展模塊的實體. 參閱EF Core遷移文檔瞭解如何使用 ObjectExtensionManager.
  • 對於 MongoDB, 它以 常規字段 存儲, 因爲 MongoDB 天生支持這種 額外 系統.

討論額外的屬性

如果你使用可重複使用的模塊,其中定義了一個實體,你想使用簡單的方式get/set此實體相關的一些數據,那麼額外的屬性系統是非常有用的. 你通常 不需要 爲自己的實體使用這個系統,是因爲它有以下缺點:

  • 它不是完全類型安全的,因爲它使用字符串用作屬性名稱.
  • 這些屬性不容易自動映射到其他對象.
  • 不會爲EF Core在數據庫表中創建字段,因此在數據庫中針對這個字段創建索引或搜索/排序並不容易.

額外屬性背後的實體

IHasExtraProperties 不限於與實體一起使用. 你可以爲任何類型的類實現這個接口,使用 GetProperty,SetProperty 和其他相關方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章