一.項目分析
在上篇中介紹了什么是"干凈架構(gòu)"鱼鸠,DDD符合了這種干凈架構(gòu)的特點匿乃,重點描述了DDD架構(gòu)遵循的依賴倒置原則雹姊,使軟件達到了低藕合。eShopOnWeb項目是學(xué)習(xí)DDD領(lǐng)域模型架構(gòu)的一個很好案例耐朴,本篇繼續(xù)分析該項目各層的職責功能众弓,主要掌握ApplicationCore領(lǐng)域?qū)觾?nèi)部的術(shù)語、成員職責隔箍。
1. web層介紹
eShopOnWeb項目與Equinox項目谓娃,雙方在表現(xiàn)層方面對比,沒有太大區(qū)別蜒滩。都是遵循了DDD表現(xiàn)層的功能職責滨达。有一點差異的是eShopOnWeb把表現(xiàn)層和應(yīng)用服務(wù)層集中在了項目web層下,這并不影響DDD風(fēng)格架構(gòu)俯艰。
項目web表現(xiàn)層引用了ApplicationCore領(lǐng)域?qū)雍虸nfrastructure基礎(chǔ)設(shè)施層捡遍,這種引用依賴是正常的。引用Infrastructure層是為了添加EF上下文以及Identity用戶管理竹握。 引用ApplicationCore層是為了應(yīng)用程序服務(wù) 調(diào)用 領(lǐng)域服務(wù)處理領(lǐng)域業(yè)務(wù)画株。
在DDD架構(gòu)下依賴關(guān)系重點強調(diào)的是領(lǐng)域?qū)拥莫毩ⅲI(lǐng)域?qū)邮峭膱A中最核心的層啦辐,所以在eShopOnWeb項目中谓传,ApplicationCore層并沒有依賴引用項目其它層。再回頭看Equinox項目芹关,領(lǐng)域?qū)右膊恍枰蕾囈庙椖科渌鼘印?/p>
下面web混合了MVC和Razor续挟,結(jié)構(gòu)目錄如下所示:
(1) Health checks
Health checks是ASP.NET Core的特性,用于可視化web應(yīng)用程序的狀態(tài)侥衬,以便開發(fā)人員可以確定應(yīng)用程序是否健康诗祸。運行狀況檢查端點/health。
//添加服務(wù)
services.AddHealthChecks()
.AddCheck<HomePageHealthCheck>("home_page_health_check")
.AddCheck<ApiHealthCheck>("api_health_check");
//添加中間件
app.UseHealthChecks("/health");
下圖檢查了web首頁和api接口的健康狀態(tài)轴总,如下圖所示
(2) Extensions
向現(xiàn)有對象添加輔助方法直颅。該Extensions文件夾有兩個類,包含用于電子郵件發(fā)送和URL生成的擴展方法怀樟。
(3) 緩存
對于Web層獲取數(shù)據(jù)庫的數(shù)據(jù)功偿,如果數(shù)據(jù)不會經(jīng)常更改,可以使用緩存漂佩,避免每次請求頁面時脖含,都去讀取數(shù)據(jù)庫數(shù)據(jù)。這里用的是本機內(nèi)存緩存投蝉。
//緩存接口類
private readonly IMemoryCache _cache;
// 添加服務(wù)养葵,緩存類實現(xiàn)
services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();
//添加服務(wù),非緩存的實現(xiàn)
//services.AddScoped<ICatalogViewModelService, CatalogViewModelService>();
2. ApplicationCore層
ApplicationCore是領(lǐng)域?qū)哟窭拢琼椖恐凶钪匾顝?fù)雜的一層关拒。ApplicationCore層包含應(yīng)用程序的業(yè)務(wù)邏輯,此業(yè)務(wù)邏輯包含在領(lǐng)域模型中。領(lǐng)域?qū)又R在Equinox項目中并沒有講清楚着绊,這里在重點解析領(lǐng)域?qū)觾?nèi)部成員谐算,并結(jié)合項目來說清楚。
下面講解領(lǐng)域?qū)觾?nèi)部的成員職責描述定義归露,參考了“Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計 第二版”洲脂。
領(lǐng)域?qū)觾?nèi)部包括:領(lǐng)域模型和領(lǐng)域服務(wù)二大塊。涉及到的術(shù)語:
領(lǐng)域模型(模型)
1)模塊
2)領(lǐng)域?qū)嶓w(也叫"實體")
3)值對象
4)聚合
領(lǐng)域服務(wù)(也叫"服務(wù)")
倉儲
下面是領(lǐng)域?qū)又饕某蓡T:
下面是聚合與領(lǐng)域模型的關(guān)系剧包。最終領(lǐng)域模型包含了:聚合恐锦、單個實體、值對象的結(jié)合疆液。
(1) 領(lǐng)域模型
領(lǐng)域模型是提供業(yè)務(wù)領(lǐng)域的概念視圖一铅,它由實體和值對象構(gòu)成。在下圖中Entities文件夾是領(lǐng)域模型堕油,可以看到包含了聚合潘飘、實體、值對象掉缺。
1.1 模塊
模塊是用來組織領(lǐng)域模型卜录,在.net中領(lǐng)域模型通過命令空間組織,模塊也就是命名空間攀圈,用來組織類庫項目里的類暴凑。比如:
namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate
namespace Microsoft.eShopWeb.ApplicationCore.Entities.BuyerAggregate
1.2 實體
實體通常由數(shù)據(jù)和行為構(gòu)成峦甩。如果要在整個生命周期的上下文里唯一跟蹤它赘来,這個對象就需要一個身份標識(ID主鍵),并看成實體凯傲。 如下所示是一個實體:
/// <summary>
/// 領(lǐng)域?qū)嶓w都有唯一標識犬辰,這里用ID做唯一標識
/// </summary>
public class BaseEntity
{
public int Id { get; set; }
}
/// <summary>
/// 領(lǐng)域?qū)嶓w,該實體行為由Basket聚合根來操作
/// </summary>
public class BasketItem : BaseEntity
{
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
public int CatalogItemId { get; set; }
}
1.3 值對象
值對象和實體都由.net 類構(gòu)成冰单。值對象是包含數(shù)據(jù)的類幌缝,沒有行為,可能有方法本質(zhì)上是輔助方法诫欠。值對象不需要身份標識涵卵,因為它們不會改變狀態(tài)。如下所示是一個值對象
/// <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 聚合
在開發(fā)中單個實體總是互相引用,聚合的作用是把相關(guān)邏輯的實體組合當作一個整體對待被廓。聚合是一致性(事務(wù)性)的邊界坏晦,對領(lǐng)域模型進行分組和隔離。聚合是關(guān)聯(lián)的對象(實體)群,放在一個聚合容器中昆婿,用于數(shù)據(jù)更改的目的球碉。每個聚合通常被限制于2~3個對象。聚合根在整個領(lǐng)域模型都可見仓蛆,而且可以直接引用睁冬。
/// <summary>
/// 定義聚合根,嚴格來說這個接口不需要任務(wù)功能看疙,它是一個普通標記接口
/// </summary>
public interface IAggregateRoot
{
}
/// <summary>
/// 創(chuàng)建購物車聚合根痴突,通常實現(xiàn)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();
//...
}
在該項目中領(lǐng)域模型與“Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計第二版”書中描述的職責有不一樣地方狼荞,來看一下:
(1) 領(lǐng)域服務(wù)有直接引用聚合中的實體(如:BasketItem)辽装。書中描述是聚合中實體不能從聚合之處直接引用,應(yīng)用把聚合看成一個整體相味。
(2) 領(lǐng)域?qū)嶓w幾乎都是貧血模型拾积。書中描述是領(lǐng)域?qū)嶓w應(yīng)該包括行為和數(shù)據(jù)。
(2) 領(lǐng)域服務(wù)
領(lǐng)域服務(wù)類方法實現(xiàn)領(lǐng)域邏輯丰涉,不屬于特定聚合中(聚合是屬于領(lǐng)域模型的)拓巧,很可能跨多個實體。當一塊業(yè)務(wù)邏輯無法融入任何現(xiàn)有聚合一死,而聚合又無法通過重新設(shè)計適應(yīng)操作時肛度,就需要考慮使用領(lǐng)域服務(wù)。下圖是領(lǐng)域服務(wù)文件夾:
/// <summary>
/// 下面是創(chuàng)建訂單服務(wù)投慈,用到的實體包括了:Basket承耿、BasketItem、OrderItem伪煤、Order跨越了多個聚合加袋,該業(yè)務(wù)放在領(lǐng)域服務(wù)中完全正確。
/// </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企業(yè)級應(yīng)用架構(gòu)設(shè)計第二版”書中描述的領(lǐng)域服務(wù)職責不完全一樣抱既,來看一下:
(1) 項目中职烧,領(lǐng)域服務(wù)只是用來執(zhí)行領(lǐng)域業(yè)務(wù)邏輯,包括了訂單服務(wù)OrderService和購物車服務(wù)BasketService防泵。書中描述是可能跨多個實體蚀之。當一塊業(yè)務(wù)邏輯無法融入任何現(xiàn)有聚合。
[](javascript:void(0); "復(fù)制代碼")
/// <summary>
/// 添加購物車服務(wù)捷泞,沒有跨越多個聚合足删,應(yīng)該不放在領(lǐng)域服務(wù)中。
/// </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項目雖然沒有完全遵循領(lǐng)域?qū)又幸佳撸蓡T職責描述拭卿,但可以理解是在代碼上簡化了領(lǐng)域?qū)拥膹?fù)雜性。
(3) 倉儲
倉儲是協(xié)調(diào)領(lǐng)域模型和數(shù)據(jù)映射層的組件贱纠。倉儲是領(lǐng)域服務(wù)中最常見類型峻厚,它負責持久化。倉儲接口的實現(xiàn)屬于基礎(chǔ)設(shè)施層谆焊。倉儲通郴萏遥基于一個IRepository接口。 下面看下項目定義的倉儲接口辖试。
/// <summary>
/// T是領(lǐng)域?qū)嶓w辜王,是BaseEntity類型的實體
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IAsyncRepository<T> where T : BaseEntity
{
Task<T> GetByIdAsync(int id);
Task<IReadOnlyList<T>> ListAllAsync();
//使用領(lǐng)域規(guī)則查詢
Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec);
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
//使用領(lǐng)域規(guī)則查詢
Task<int> CountAsync(ISpecification<T> spec);
}
(4) 領(lǐng)域規(guī)則
在倉儲設(shè)計查詢接口時,可能還會用到領(lǐng)域規(guī)則罐孝。 在倉儲中一般都是定義固定的查詢接口,如上面?zhèn)}儲的IAsyncRepository所示呐馆。而復(fù)雜的查詢條件可能需要用到領(lǐng)域規(guī)則。在本項目中通過強大Linq 表達式樹Expression 來實現(xiàn)動態(tài)查詢莲兢。
/// <summary>
/// 領(lǐng)域規(guī)則接口汹来,由BaseSpecification實現(xiàn)
/// 最終由Infrastructure.Data.SpecificationEvaluator<T>類來構(gòu)建完整的表達樹
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ISpecification<T>
{
//創(chuàng)建一個表達樹,并通過where首個條件縮小查詢范圍改艇。
//實現(xiàn):IQueryable<T> query = query.Where(specification.Criteria)
Expression<Func<T, bool>> Criteria { get; }
//基于表達式的包含
//實現(xiàn)如: 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文件夾中定義的接口收班,都由基礎(chǔ)設(shè)施層來實現(xiàn)。如:
IAppLogger日志接口
IEmailSender郵件接口
IAsyncRepository倉儲接口
3.Infrastructure層
基礎(chǔ)設(shè)施層Infrastructure依賴于ApplicationCore谒兄,這遵循依賴倒置原則(DIP)摔桦,Infrastructure中代碼實現(xiàn)了ApplicationCore中定義的接口(Interfaces文件夾)承疲。該層沒有太多要講的赊豌,功能主要包括:使用EF Core進行數(shù)據(jù)訪問熙兔、Identity麸锉、日志柳爽、郵件發(fā)送磷脯。與Equinox項目的基礎(chǔ)設(shè)施層差不多柿赊,區(qū)別多了領(lǐng)域規(guī)則诡蜓。
領(lǐng)域規(guī)則SpecificationEvaluator.cs類用來構(gòu)建查詢表達式(Linq expression)洽腺,該類返回IQueryable<T>類型核无。IQueryable接口并不負責查詢的實際執(zhí)行炼彪,它所做的只是描述要執(zhí)行的查詢拷橘。
public class EfRepository<T> : IAsyncRepository<T> where T : BaseEntity
{
//...這里省略的是常規(guī)查詢冗疮,如ADDAsync、UpdateAsync诅挑、GetByIdAsync ...
//獲取構(gòu)建的查詢表達式
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:對每個元素調(diào)用的累加器函數(shù)
//返回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企業(yè)級應(yīng)用架構(gòu)設(shè)計 第二版