一、簡要介紹 ABP vNext 針對於應用服務層,為我們單獨設計了一個模塊進行實現,即 Volo.Abp.Ddd.Application 模塊。 PS:最近博主也是在惡補 DDD 相關的知識,這裡推薦大家看一下 "ThoughtWorks" 的 DDD 相關文章。 關於 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
介面,並提供了基本組件的簡單基類。而後者則是定義了 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 當中,應用服務是表達 用戶用例 和 用戶故事 的主要手段,應用服務只是通過領域對象/領域服務來表達需求用例的一個組件。不要將業務邏輯泄漏到應用服務當中,這種設計最終會導致貧血領域模型。