DDD战术落地—聚合的编排一定要在应用层吗?(领域服务与领域对象的区别)

先说结论:我个人认为不是的。

悖论

网上很多文章给出的结论:

  1. 推荐聚合划分越细越好;
  2. 聚合之间的编排应该放在应用层完成;

但是这就出现了一个悖论,即一些核心的领域逻辑分在了两个聚合中,但是却只能在应用层去组合。造成了领域能力的外泄。

举例

举个“登录操作”例子:

登录时,有“租户”和“用户名/密码”。用户登录时,首先判断是否在某个租户下,然后在判断该租户下用户名/密码是否相同。

因为可以增删改租户信息,也可以增删改用户信息,所以租户和用户应该划分为两个聚合。

领域建模图.png

那么登录这一个行为:具体到代码就会变成了:

boolean authentic=false
//获取租户
Tenant tenant= TenantRepo.tenantOfId(aTenantId);
if(tenant!=null && tenant.isActive()){
    User user=UserRepo.userWithUserName(aTenantId,aUserName);
    if(user!=null){
        authentic=user.isAuthentic(aPassword);
    }
}

但是仔细想下,这个行为应该属于“认证授权”的行为,需要放在应用层进行编排处理吗?它会使得调用者的逻辑非常复杂。强加在客户端上的职责应该在我们的领域模型中予以解决。只与领域相关的信息决不能泄露到客户端。即使客户端是一个应用服务,它也不应该负责身份与访问权限的管理。

客户端(应用层)需要处理的唯一业务职责:调用单个业务操作,而由改业务操作去处理所有的业务细节。

解决方案:提供领域服务

提供一个领域服务,来聚合多个领域对象。

UserDescriptor userDescriptor=authenticationDomainService.authenticate(aTenantId,aUserName,aPassword);

客户端只需要获取一个无状态的AuthenticationDomainService,然后调用authenticate,这种方式将所有的认证细节放在领域服务,而非应用服务。在需要的情况下,领域服务可使用任何领域对象完成操作,包括对密码的加密。

客户端无需知道任何的细节。

通用语言也得到了满足,因为我们将所有的领域术语都放在了身份管理这个领域,而非一部分放在领域模型,另一部分放在客户端(应用层)。

领域服务方法返回了一个UserDescriptor值对象,这是一个很小的对象,并且是安全的,与User相比,它值包含了3个关键属性。

public class UserDescriptor{
  private String emailAddress;
  private long tenantId;
  private String userName;
}

进阶:何时应该使用领域服务

领域服务到底是什么?当领域中某个操作或者转换过程不是实体或者值对象的职责时,此时便应该将该操作放在一个单独的接口,即领域服务。

请保证领域服务和通用语言是一致的,并且保证它是无状态的;

通常领域模型主要关注特定某个领域的业务,同样,领域服务也具有相似特点。由于领域服务有可能在单个原子操作中处理多个领域对象,这将增加领域服务的复杂性。

那么何时一个操作不属于实体或值对象?即何时可使用领域服务:

  • 执行一个显著的业务操作过程;
  • 对领域对象进行转换;
  • 以多个领域对象作为输入进行计算,产生一个值对象结果;

当一个方法不便放在实体或者值对象,使用领域服务便是最佳的解决方案。

领域服务与应用服务的区别

  • 应用服务并不处理业务逻辑;
  • 领域服务恰恰是处理业务逻辑;

缺点:确定需要领域服务

过度使用领域服务将导致贫血领域模型,即所有的业务逻辑都位于领域服务中,而非实体和值对象。

文章参考

DDD领域驱动设计实战(六)-领域服务

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。