在上篇中介紹了什麼是"乾淨架構",DDD符合了這種乾淨架構的特色,重點描述了DDD架構遵循的依賴倒置原則,使軟件達到了低藕合。eShopOnWeb項目是學習DDD領域模型架構的一個很好案例,本篇繼續分析該項目各層的職責功能,主要掌握ApplicationCore領域層內部的術語、成員職責。web
eShopOnWeb項目與Equinox項目,雙方在表現層方面對比,沒有太大區別。都是遵循了DDD表現層的功能職責。有一點差別的是eShopOnWeb把表現層和應用服務層集中在了項目web層下,這並不影響DDD風格架構。數據庫
項目web表現層引用了ApplicationCore領域層和Infrastructure基礎設施層,這種引用依賴是正常的。引用Infrastructure層是爲了添加EF上下文以及Identity用戶管理。 引用ApplicationCore層是爲了應用程序服務 調用 領域服務處理領域業務。express
在DDD架構下依賴關係重點強調的是領域層的獨立,領域層是同心圓中最核心的層,因此在eShopOnWeb項目中,ApplicationCore層並無依賴引用項目其它層。再回頭看Equinox項目,領域層也不須要依賴引用項目其它層。windows
下面web混合了MVC和Razor,結構目錄以下所示:api
(1) Health checks 緩存
Health checks是ASP.NET Core的特性,用於可視化web應用程序的狀態,以便開發人員能夠肯定應用程序是否健康。運行情況檢查端點/health。架構
//添加服務 services.AddHealthChecks() .AddCheck<HomePageHealthCheck>("home_page_health_check") .AddCheck<ApiHealthCheck>("api_health_check"); //添加中間件 app.UseHealthChecks("/health");
下圖檢查了web首頁和api接口的健康狀態,以下圖所示app
(2) Extensionsasync
向現有對象添加輔助方法。該Extensions
文件夾有兩個類,包含用於電子郵件發送和URL生成的擴展方法。函數
(3) 緩存
對於Web層獲取數據庫的數據,若是數據不會常常更改,可使用緩存,避免每次請求頁面時,都去讀取數據庫數據。這裏用的是本機內存緩存。
//緩存接口類 private readonly IMemoryCache _cache; // 添加服務,緩存類實現 services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>(); //添加服務,非緩存的實現 //services.AddScoped<ICatalogViewModelService, CatalogViewModelService>();
ApplicationCore是領域層,是項目中最重要最複雜的一層。ApplicationCore層包含應用程序的業務邏輯,此業務邏輯包含在領域模型中。領域層知識在Equinox項目中並無講清楚,這裏在重點解析領域層內部成員,並結合項目來講清楚。
下面講解領域層內部的成員職責描述定義,參考了「Microsoft.NET企業級應用架構設計 第二版」。
領域層內部包括:領域模型和領域服務二大塊。涉及到的術語:
領域模型(模型)
1)模塊
2)領域實體(也叫"實體")
3)值對象
4)聚合
領域服務(也叫"服務")
倉儲
下面是領域層主要的成員:
下面是聚合與領域模型的關係。最終領域模型包含了:聚合、單個實體、值對象的結合。
(1) 領域模型
領域模型是提供業務領域的概念視圖,它由實體和值對象構成。在下圖中Entities文件夾是領域模型,能夠看到包含了聚合、實體、值對象。
1.1 模塊
模塊是用來組織領域模型,在.net中領域模型經過命令空間組織,模塊也就是命名空間,用來組織類庫項目裏的類。好比:
namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate
1.2 實體
實體一般由數據和行爲構成。若是要在整個生命週期的上下文裏惟一跟蹤它,這個對象就須要一個身份標識(ID主鍵),並當作實體。 以下所示是一個實體:
/// <summary> /// 領域實體都有惟一標識,這裏用ID作惟一標識 /// </summary> public class BaseEntity { public int Id { get; set; } } /// <summary> /// 領域實體,該實體行爲由Basket聚合根來操做 /// </summary> public class BasketItem : BaseEntity { public decimal UnitPrice { get; set; } public int Quantity { get; set; } public int CatalogItemId { get; set; } }
1.3 值對象
值對象和實體都由.net 類構成。值對象是包含數據的類,沒有行爲,可能有方法本質上是輔助方法。值對象不須要身份標識,由於它們不會改變狀態。以下所示是一個值對象
/// <summary> /// 訂單地址 值對象是普通的DTO類,沒有惟一標識。 /// </summary> public class Address // ValueObject { public String Street { get; private set; } public String City { get; private set; } public String State { get; private set; } public String Country { get; private set; } public String ZipCode { get; private set; } private Address() { } public Address(string street, string city, string state, string country, string zipcode) { Street = street; City = city; State = state; Country = country; ZipCode = zipcode; } }
1.4 聚合
在開發中單個實體老是互相引用,聚合的做用是把相關邏輯的實體組合看成一個總體對待。聚合是一致性(事務性)的邊界,對領域模型進行分組和隔離。聚合是關聯的對象(實體)羣,放在一個聚合容器中,用於數據更改的目的。每一個聚合一般被限制於2~3個對象。聚合根在整個領域模型均可見,並且能夠直接引用。
/// <summary> /// 定義聚合根,嚴格來講這個接口不須要任務功能,它是一個普通標記接口 /// </summary> public interface IAggregateRoot { } /// <summary> /// 建立購物車聚合根,一般實現IAggregateRoot接口 /// 購物車聚合模型(包括Basket、BasketItem實體) /// </summary> public class Basket : BaseEntity, IAggregateRoot { public string BuyerId { get; set; } private readonly List<BasketItem> _items = new List<BasketItem>(); public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly(); //... }
在該項目中領域模型與「Microsoft.NET企業級應用架構設計第二版」書中描述的職責有不同地方,來看一下:
(1) 領域服務有直接引用聚合中的實體(如:BasketItem)。書中描述是聚合中實體不能從聚合之處直接引用,應用把聚合當作一個總體。
(2) 領域實體幾乎都是貧血模型。書中描述是領域實體應該包括行爲和數據。
(2) 領域服務
領域服務類方法實現領域邏輯,不屬於特定聚合中(聚合是屬於領域模型的),極可能跨多個實體。當一塊業務邏輯沒法融入任何現有聚合,而聚合又沒法經過從新設計適應操做時,就須要考慮使用領域服務。下圖是領域服務文件夾:
/// <summary> /// 下面是建立訂單服務,用到的實體包括了:Basket、BasketItem、OrderItem、Order跨越了多個聚合,該業務放在領域服務中徹底正確。 /// </summary> /// <param name="basketId">購物車ID</param> /// <param name="shippingAddress">訂單地址</param> /// <returns>回返回類型</returns> public async Task CreateOrderAsync(int basketId, Address shippingAddress) { var basket = await _basketRepository.GetByIdAsync(basketId); Guard.Against.NullBasket(basketId, basket); var items = new List<OrderItem>(); foreach (var item in basket.Items) { var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId); var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri); var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity); items.Add(orderItem); } var order = new Order(basket.BuyerId, shippingAddress, items); await _orderRepository.AddAsync(order); }
在該項目與「Microsoft.NET企業級應用架構設計第二版」書中描述的領域服務職責不徹底同樣,來看一下:
(1) 項目中,領域服務只是用來執行領域業務邏輯,包括了訂單服務OrderService和購物車服務BasketService。書中描述是可能跨多個實體。當一塊業務邏輯沒法融入任何現有聚合。
/// <summary> /// 添加購物車服務,沒有跨越多個聚合,應該不放在領域服務中。 /// </summary> /// <param name="basketId"></param> /// <param name="catalogItemId"></param> /// <param name="price"></param> /// <param name="quantity"></param> /// <returns></returns> public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity) { var basket = await _basketRepository.GetByIdAsync(basketId); basket.AddItem(catalogItemId, price, quantity); await _basketRepository.UpdateAsync(basket); }
總的來講,eShopOnWeb項目雖然沒有徹底遵循領域層中,成員職責描述,但能夠理解是在代碼上簡化了領域層的複雜性。
(3) 倉儲
倉儲是協調領域模型和數據映射層的組件。倉儲是領域服務中最多見類型,它負責持久化。倉儲接口的實現屬於基礎設施層。倉儲一般基於一個IRepository接口。 下面看下項目定義的倉儲接口。
/// <summary> /// T是領域實體,是BaseEntity類型的實體 /// </summary> /// <typeparam name="T"></typeparam> public interface IAsyncRepository<T> where T : BaseEntity { Task<T> GetByIdAsync(int id); Task<IReadOnlyList<T>> ListAllAsync(); //使用領域規則查詢 Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec); Task<T> AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); //使用領域規則查詢 Task<int> CountAsync(ISpecification<T> spec); }
(4) 領域規則
在倉儲設計查詢接口時,可能還會用到領域規則。 在倉儲中通常都是定義固定的查詢接口,如上面倉儲的IAsyncRepository所示。而複雜的查詢條件可能須要用到領域規則。在本項目中經過強大Linq 表達式樹Expression 來實現動態查詢。
/// <summary> /// 領域規則接口,由BaseSpecification實現 /// 最終由Infrastructure.Data.SpecificationEvaluator<T>類來構建完整的表達樹 /// </summary> /// <typeparam name="T"></typeparam> public interface ISpecification<T> { //建立一個表達樹,並經過where首個條件縮小查詢範圍。 //實現:IQueryable<T> query = query.Where(specification.Criteria) Expression<Func<T, bool>> Criteria { get; } //基於表達式的包含 //實現如: Includes(b => b.Items) List<Expression<Func<T, object>>> Includes { get; } List<string> IncludeStrings { get; } //排序和分組 Expression<Func<T, object>> OrderBy { get; } Expression<Func<T, object>> OrderByDescending { get; } Expression<Func<T, object>> GroupBy { get; } //查詢分頁 int Take { get; } int Skip { get; } bool isPagingEnabled { get;} }
最後Interfaces文件夾中定義的接口,都由基礎設施層來實現。如:
IAppLogger日誌接口
IEmailSender郵件接口
IAsyncRepository倉儲接口
基礎設施層Infrastructure依賴於ApplicationCore,這遵循依賴倒置原則(DIP),Infrastructure中代碼實現了ApplicationCore中定義的接口(Interfaces文件夾)。該層沒有太多要講的,功能主要包括:使用EF Core進行數據訪問、Identity、日誌、郵件發送。與Equinox項目的基礎設施層差很少,區別多了領域規則。
領域規則SpecificationEvaluator.cs類用來構建查詢表達式(Linq expression),該類返回IQueryable<T>類型。IQueryable接口並不負責查詢的實際執行,它所作的只是描述要執行的查詢。
public class EfRepository<T> : IAsyncRepository<T> where T : BaseEntity { //...這裏省略的是常規查詢,如ADDAsync、UpdateAsync、GetByIdAsync ... //獲取構建的查詢表達式 private IQueryable<T> ApplySpecification(ISpecification<T> spec) { return SpecificationEvaluator<T>.GetQuery(_dbContext.Set<T>().AsQueryable(), spec); } }
public class SpecificationEvaluator<T> where T : BaseEntity { /// <summary> /// 作查詢時,把返回類型IQueryable看成通貨 /// </summary> /// <param name="inputQuery"></param> /// <param name="specification"></param> /// <returns></returns> public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> specification) { var query = inputQuery; // modify the IQueryable using the specification's criteria expression if (specification.Criteria != null) { query = query.Where(specification.Criteria); } // Includes all expression-based includes //TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func); //seed:query初始的聚合值 //func:對每一個元素調用的累加器函數 //返回TAccumulate:累加器的最終值 //https://msdn.microsoft.com/zh-cn/windows/desktop/bb549218 query = specification.Includes.Aggregate(query, (current, include) => current.Include(include)); // Include any string-based include statements query = specification.IncludeStrings.Aggregate(query, (current, include) => current.Include(include)); // Apply ordering if expressions are set if (specification.OrderBy != null) { query = query.OrderBy(specification.OrderBy); } else if (specification.OrderByDescending != null) { query = query.OrderByDescending(specification.OrderByDescending); } if (specification.GroupBy != null) { query = query.GroupBy(specification.GroupBy).SelectMany(x => x); } // Apply paging if enabled if (specification.isPagingEnabled) { query = query.Skip(specification.Skip) .Take(specification.Take); } return query; } }
參考資料
Microsoft.NET企業級應用架構設計 第二版