什麼是DDD?
ABP框架提供了基礎設施, 使基於領域驅動設計的開發更易實現.
領域驅動設計(DDD) 是一種透過將實現連線到持續進化的模型來滿足複雜需求的軟體開發方法.
領域驅動設計的前提是:
把專案的主要重點放在核心領域和領域邏輯上把複雜的設計放在領域模型上發起技術專家和領域專家之間的創造性協作,以迭代方式完善解決特定領域問題的概念模型分層ABP框架遵循DDD原則和模式去實現分層應用程式模型,該模型由四個基本層組成:
表示層: 為使用者提供介面. 使用應用層實現與使用者互動.應用層: 表示層與領域層的中介,編排業務物件執行特定的應用程式任務. 使用應用程式邏輯實現用例.領域層: 包含業務物件以及業務規則. 是應用程式的核心.基礎設施層: 提供通用的技術功能,支援更高的層,主要使用第三方類庫.領域層-實體&聚合根實體是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型別.可以是其他型別如string, int, long或其他你需要的型別.
具有Guid主鍵的實體
如果你的實體Id型別為 Guid,有一些好的實踐可以實現:
建立一個建構函式,獲取ID作為引數傳遞給基類.如果沒有為GUID Id賦值,ABP框架會在儲存時設定它,但是在將實體儲存到資料庫之前最好在實體上有一個有效的Id.如果使用帶引數的建構函式建立實體,那麼還要建立一個 private 或 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 }; }}
上面的例子中,複合鍵由UserId和RoleId組成.在關係資料庫中,它是相關表的複合主鍵. 具有複合鍵的實體應當實現上面程式碼中所示的GetKeys()方法.
需要注意,複合主鍵實體不可以使用 IRepository<TEntity, TKey> 介面,因為它需要一個唯一的Id屬性. 但你可以使用 IRepository<TEntity>
聚合根
聚合是領域驅動設計中的一種模式.DDD的聚合是一組可以作為一個單元處理的域物件.
例如,訂單及訂單系列的商品,這些是獨立的物件,但將訂單(連同訂單系列的商品)視為一個聚合通常是很有用的
AggregateRoot<TKey>類繼承自Entity<TKey>類,所以預設有Id這個屬性, ABP 會預設為聚合根建立倉儲,當然,ABP也可以為所有的實體建立倉儲.
ABP不強制你使用聚合根,實際上你可以使用上面定義的Entity類,當然,如果你想實現領域驅動設計並且建立聚合根,這裡有一些最佳實踐僅供參考:
這是一個具有子實體集合的聚合根例子:
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 來構造一個"訂單"例項.因此,在沒有Id和referenceNo的時候是無法建立訂單的.protected/private的建構函式只有從資料庫讀取物件時 反序列化 才需要.OrderLine的建構函式是internal的,所以它只能由領域層來建立.在Order.AddProduct這個方法的內部被使用.Order.AddProduct實現了業務規則將商品新增到訂單中所有屬性都有protected的set.這是為了防止實體在實體外部任意改變.因此,在沒有向訂單中新增新產品的情況下設定 TotalItemCount將是危險的.它的值由AddProduct方法維護.ABP框架不強制你應用任何DDD規則或模式.但是,當你準備應用的DDD規則或模式時候,ABP會讓這變的可能而且更簡單.文件同樣遵循這個原則.
帶有組合鍵的聚合根
雖然這種聚合根並不常見(也不建議使用),但實際上可以按照與上面提到的跟實體相同的方式定義複合鍵.在這種情況下,要使用非泛型的AggregateRoot基類.
BasicAggregateRoot類
AggregateRoot 類實現了 IHasExtraProperties 和 IHasConcurrencyStamp 介面,這為派生類帶來了兩個屬性. IHasExtraProperties 使實體可擴充套件和 IHasConcurrencyStamp 添加了由ABP框架管理的 ConcurrencyStamp 屬性實現樂觀併發. 在大多數情況下,這些是聚合根需要的功能.
但是,如果你不需要這些功能,你的聚合根可以繼承 BasicAggregateRoot<TKey>(或BasicAggregateRoot).
基類和介面的審計屬性有一些屬性,像CreationTime,CreatorId,LastModificationTime...在所有應用中都很常見. ABP框架提供了一些介面和基類來標準化這些屬性,並自動設定它們的值.
審計介面有很多的審計介面,你可以實現一個你需要的那個.
IHasCreationTime 定義了以下屬性:CreationTimeIMayHaveCreator 定義了以下屬性:CreatorIdICreationAuditedObject 繼承 IHasCreationTime 和 IMayHaveCreator, 所以它定義了以下屬性:CreationTimeCreatorIdIHasModificationTime 定義了以下屬性:LastModificationTimeIModificationAuditedObject 擴充套件 IHasModificationTime 並添加了 LastModifierId 屬性. 所以它定義了以下屬性:LastModificationTimeLastModifierIdIAuditedObject 擴充套件 ICreationAuditedObject 和 IModificationAuditedObject, 所以它定義了以下屬性:CreationTimeCreatorIdLastModificationTimeLastModifierIdISoftDelete 定義了以下屬性:IsDeletedIHasDeletionTime 擴充套件 ISoftDelete 並添加了 DeletionTime 屬性. 所以它定義了以下屬性:IsDeletedDeletionTimeIDeletionAuditedObject 擴充套件 IHasDeletionTime 並添加了 DeleterId 屬性. 所以它定義了以下屬性:IsDeletedDeletionTimeDeleterIdIFullAuditedObject 繼承 IAuditedObject 和 IDeletionAuditedObject, 所以它定義了以下屬性:CreationTimeCreatorIdLastModificationTimeLastModifierIdIsDeletedDeletionTimeDeleterId當你實現了任意介面,或者從下一節定義的類派生,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),你必須真正實現聚合模式).