[Abp vNext 原始碼分析] - 6. DDD 的應用層支援 (應用服務)
一、簡要介紹
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
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 當中,應用服務是表達 使用者用例 和 使用者故事 的主要手段,應用服務只是通過領域物件/領域服務來表達需求用例的一個元件。不要將業務邏輯洩漏到應用服務當中,這種設計最終會導致貧血領域模型。