一、簡要介紹
ABP vNext 針對於應用服務層,為我們單獨設計了一個模塊進行實現,即 Volo.Abp.Ddd.Application 模塊。
PS:最近博主也是在惡補 DDD 相關的知識,這里推薦大家看一下 ThoughtWorks 的 DDD 相關文章。
關於 DDD 相關的著作,我這兒還是推薦經典的那三本《領域驅動設計:軟件核心復雜性應對之道》、《實現領域驅動設計》、《領域驅動設計精粹》。
DDD 的學習整體來說是比較枯燥的,而且偏理論化的知識。所以需要結合大量實例來看,反復對照書中的概念加深理解。不僅要看別人的實例,自己也要嘗試運用 DDD 的戰略方法和戰術方法進行設計。
應用服務層在 DDD 分層架構里面是最頂層的,一般與前端(展示層)打交道的都是應用服務層。常規的開發人員,如果沒有遵循 DDD 理論來進行開發的話,應用服務層是十分臃腫的,里面全是業務邏輯。而領域層里面則是空無一物,全是貧血的領域模型對象。這種模式被稱之為 貧血領域模型模式,這是一個 反模式。
這里我就不再贅述應用服務層與 DDD 之間的關系了,在這里你可以看作它是一個 API 接口實現類,你所有對外開放的接口都是通過應用服務層暴露的,接口的方法應該與用例相對應。
二、源碼分析
應用服務層模塊里面比較簡單,只有兩個文件夾,分別存放了數據傳輸模型(Dtos)和應用服務基類定義(Services)。
2.1 啟動模塊
首先我們還是按照之前的順序,看一個模塊先看他的模塊類。這里我們先看一下 AbpDddApplicationModule
的代碼。
[DependsOn(
typeof(AbpDddDomainModule),
typeof(AbpSecurityModule),
typeof(AbpObjectMappingModule),
typeof(AbpValidationModule),
typeof(AbpAuthorizationModule),
typeof(AbpHttpAbstractionsModule),
typeof(AbpSettingsModule),
typeof(AbpFeaturesModule)
)]
// 不要看上面依賴這么多模塊,主要是因為基類會用到很多基礎組件。
public class AbpDddApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// 配置接口類型。
Configure<ApiDescriptionModelOptions>(options =>
{
options.IgnoredInterfaces.AddIfNotContains(typeof(IRemoteService));
options.IgnoredInterfaces.AddIfNotContains(typeof(IApplicationService));
options.IgnoredInterfaces.AddIfNotContains(typeof(IUnitOfWorkEnabled));
});
}
}
可以看到,在上述代碼里面,只做了一件事情,就是調用 ApiDescriptionModelOptions
,往里面添加了 IRemoteService
、IApplicationService
、IUnitOfWOrkEnabled
三種接口類型。添加了三種類型之后,ABP vNext 根據應用服務類創建控制器時,就會從這個 IgnoredInterfaces
判斷哪些類型不被忽略 (即只會自動注冊實現了三種接口的類型成為控制器)。
2.2 應用服務基類
ABP vNext 提供了標准基類 ApplicationService
和簡單 Crud 基類 CrudAppService
給我們使用,前者只是繼承了 IApplicationService
接口,並提供了基本組件的簡單基類。而后者則是定義了 Crud 操作所需要的所有 API 方法,你只需要繼承這個基類對象,填充相應的泛型參數,就可以快速實現一個 Crud 接口。
2.2.1 簡單基類
簡單基類里面我們首先需要注意的是它實現的接口,你可以發現 ApplicationService
實現了諸多接口,不過這些接口更多的是類似於標識接口。
public abstract class ApplicationService :
IApplicationService,
IAvoidDuplicateCrossCuttingConcerns,
IValidationEnabled,
IUnitOfWorkEnabled,
IAuditingEnabled,
ITransientDependency
{
// ... 其他代碼
}
所有應用服務都必須繼承 IApplicationService
,這個是肯定的,不然 ABP vNext 不會為我們生成需要的控制器。
其次是 IAvoidDuplicateCrossCuttingConcerns
接口,這個接口最早可以追溯到老版本 ABP 框架里面。它的主要作用是防止攔截器進行重復執行。
public interface IAvoidDuplicateCrossCuttingConcerns
{
List<string> AppliedCrossCuttingConcerns { get; }
}
例如調用購買這個 API 接口,首先會進入 ASP.NET Core 的審計日志 Filter,在 Filter 里面會將這個 API 接口歸屬的類型的 List
容器(接口里面定義的 List )里面寫入一條記錄,說明已經通過審計日志過濾器記錄了。
寫了審計日志之后,又會進入審計日志攔截器,這個時候攔截器就會對指定的類型進行判斷,看是否已經被執行過了,因為這個類型的 List
容器有了之前過濾器的記錄,所以不會重復執行。
public override void Intercept(IAbpMethodInvocation invocation)
{
if (!ShouldIntercept(invocation, out var auditLog, out var auditLogAction))
{
invocation.Proceed();
return;
}
// ... 審計日志記錄。
}
protected virtual bool ShouldIntercept(
IAbpMethodInvocation invocation,
out AuditLogInfo auditLog,
out AuditLogActionInfo auditLogAction)
{
// 判斷實例的 List 容器里面,是否寫入了 AbpCrossCuttingConcerns.Auditing。
if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Auditing))
{
return false;
}
// ... 其他代碼
return true;
}
剩余的 IValidationEnabled
、IUnitOfWorkEnabled
、IAuditingEnabled
、ITransientDependency
接口類似於一個啟用標識,只要類型繼承了該接口,就會執行一些特殊的操作。
回到之前的簡單基類里面,ABP vNext 為我們注入了大量基礎設施,例如獲取當前用戶的 ICurrentUser
組件,獲取當前租戶的 ICurrentTenant
組件,還有日志組件等。
除了基礎組件,ABP vNext 在簡單基類里面還提供了一個權限檢測方法,用戶檢測當前用戶是否具備某些權限。
protected virtual async Task CheckPolicyAsync([CanBeNull] string policyName)
{
if (string.IsNullOrEmpty(policyName))
{
return;
}
await AuthorizationService.CheckAsync(policyName);
}
在不具備權限的時候,ABP vNext 會拋出 AbpAuthorizationException
異常。
2.2.2 Crud 基類
Crud 基類可以極大減少對於某些簡單對象的代碼編寫,例如我有個客戶管理接口,只需要簡單地增刪改查操作。那么我就可以直接繼承自 Crud 基類,給它填寫和是的泛型參數之后,ABP vNext 就會為我們生成帶有增刪改查操作的應用服務對象。
這個 Crud 基類擁有多個泛型定義與實現,除了真正的實現以外,其他的都是簡單的調用基類方法而已。我們直接進入主題,看一下類型簽名為 public abstract class CrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
的基類。
public abstract class CrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
: ApplicationService,
ICrudAppService<TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntity : class, IEntity<TKey>
where TGetOutputDto : IEntityDto<TKey>
where TGetListOutputDto : IEntityDto<TKey>
{
public virtual async Task<TGetOutputDto> GetAsync(TKey id)
{
// 具體代碼。
}
public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input)
{
// 具體代碼。
}
public virtual async Task<TGetOutputDto> CreateAsync(TCreateInput input)
{
// 具體代碼。
}
public virtual async Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input)
{
// 具體代碼。
}
public virtual async Task DeleteAsync(TKey id)
{
// 具體代碼。
}
}
從上述代碼可以看到基類根據傳入的泛型參數,將會為我們實現常規的增刪改查邏輯。我們也可以隨時重寫這些方法,來達到一些個性化的操作。
ABP vNext 抽象了公用接口以外,在內部還編寫了諸如 MapToEntity()
和 MapToEntity()
等內部共用方法,這里就不再詳細贅述,這些方法都是 protected
修飾的,你也可以隨時重寫來達到自己的目的。
2.3 數據傳輸對象
一般來說,應用服務層返回給展示層的數據肯定是某個實體對象的部分屬性,或者是多個聚合的整體,這個時候就需要 DTO 來幫我們處理應用服務層與外部的數據交換了。
ABP vNext 在應用服務模塊定義了常用的一些 DTO 對象,例如實體 DTO 和分頁查詢 DTO,關於這些 DTO 你只需將其看作一個數據容器即可,不需要太多關注,這里也沒有太多要講的。
三、總結
ABP vNext 提供的應用服務層模塊還是比較簡單的,里面主要是針對應用服務基類進行了預定義。方便我們開發人員進行業務開發,而不需要自己實現這些繁雜的基類。
在 DDD 當中,應用服務是表達 用戶用例 和 用戶故事 的主要手段,應用服務只是通過領域對象/領域服務來表達需求用例的一個組件。不要將業務邏輯泄漏到應用服務當中,這種設計最終會導致貧血領域模型。