#一、简要介绍
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 当中,应用服务是表达 用户用例 和 用户故事 的主要手段,应用服务只是通过领域对象/领域服务来表达需求用例的一个组件。不要将业务逻辑泄漏到应用服务当中,这种设计最终会导致贫血领域模型。