天天看點

[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

接口,并提供了基本元件的簡單基類。而後者則是定義了 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 當中,應用服務是表達 使用者用例 和 使用者故事 的主要手段,應用服務隻是通過領域對象/領域服務來表達需求用例的一個元件。不要将業務邏輯洩漏到應用服務當中,這種設計最終會導緻貧血領域模型。

四、點選我跳轉到文章目錄