请确定你是否需要一个领域服务
请不要过于倾向于将一个领域概念建模成领域服务,而是只有在有必要的时候才这么做。一不小心,我们就有可能陷人将领域服务作为“银弹”的陷进。过度地使用领域服务将导致贫血领域模型[Fowler, Anemic],即所有的业务逻辑都位于领域服务中,而不是实体和值对象中。下面的例子为我们展示了仔细思考的重要性。以这些例子为指导,你将学到应该在什么情况下使用领域服务。
让我们来看一个需要建立领域服务的例子。考虑身份与访问上下文,我们需要对一个User进行认证。回忆一下,在实体(5)章节中,我们曾遇到了一个建模场景,那时团队决定将问题延后。那时所说的“延后”便是现在了:
- 系统必须对User进行认证,并且只有当Tenant处于激活状态时才能对User 进行认证。
我们来看看为什么领域服务在此时是必要的。我们可以简单地将该认证操作放在实体上吗?从客户的角度来看,我们可能会使用以下代码来实现认证过程:
// client finds User and asks it to authenticate itself boolean authentic = false;
boolean authentic = false;
User user = DomainRegistry.userRepository().userWithUsername(aTenantId, aUsername);
if (user != null) {
authentic = user.isAuthentic(aPassword);
}
return authentic;
对于以上设计,我认为至少存在两个问题。首先,客户端需要知道某些认证细节,他们需要找到一个User,然后再对该User进行密码匹配。这种方法也不能显式地表达通用语言。这里,我们询问的是一个User “是否被认证了”,而没有表达出“认证”这个过程。在有可能的情况下,我们应该尽量使建模术语直接地表达出团队成员的交流用语。但是,还有更糟糕的。
这种建模方式并不能准确地表达出团队成员所指的“对User进行认证”的过程。它缺少了“检查Tenant是否处于激活状态”这个前提条件。如果一个User所属的Tenant处于非激活状态,我们便不应该对该User进行认证。或许我们可以通过以下方法予以解决:
//客户端查找User,然后User完成自我认证
// maybe this way is better ...
boolean authentic = false;
Tenant tenant =
DomainRegistry.tenantRepository().tenantOfId(aTenantId);
if (tenant != null && tenant.isActive()) {
User user =
DomainRegistry
.userRepository()
.userWithUsername(aTenantId, aUsername);
if (user != null) {
authentic = tenant.authenticate(user, aPassword)
}
}
return authentic;
这种方式的确对Tenant的活跃性做了检查,同时我们也将User的isAuthentic() 方法换成了 Tenant 的 authenticate()方法。
然而,这种方式也是有问题的。请看看我们带给客户端的额外负担,此时客户端需要知道更多的认证细节,而这些是他们不应该知道的。当然,我们可以将
Tenant的isActive()方法放在authenticat()方法中,但是我得说,这并不是一个显式的模型。同时,这将带来另外一个问题,即此时的Tenant需要知道如何对密码进行
操作。回忆一下该认证过程的另一个需求:
- 必须对密码进行加密,并且不能使用明文密码。
对于以上解决方案,我们似乎给模型带来了太多的问题。对于最后一种方案,我们必须从以下四种解决办法中选择一种:
- 在Tenant中处理对密码的加密,然后将加密后的密码传给User。这种方法违背了单一职责原则[Martin,SRP]。
- 由于一个User必须保证对密码的加密,它可能已经知道了一些加密信息。如果是这样,我们可以在User上创建一个方法,该方法对明文密码进行认证。但是,在这种方式下,认证过程变成了Tenant上的门面(Facade),而实际的认证功能全在UserJi。另外,User上的认证方法必须声明为protected,以防止外界客户端对认证方法的直接调用。
- Tenant依赖于User对密码进行加密,然后将加密后的密码与原有密码进行匹配。这种方法似乎在对象协作之间增加了额外的步骤。此时,Tenant依然需要知道认证细节。
- 让客户端对密码进行加密,然后将其传给Tenant。这样导致的问题在于,客户端承载了它本不应该有的职责。
以上这些方法都无济于事,同时客户端依然非常复杂。强加在客户端上的职责应该在我们自己的模型中予以处理。只与领域相关的信息决不能泄漏到客户端中去。即使客户端是一个应用服务,它也不应该负责对身份与访问权限的管理。
回想一下,客户端需要处理的唯一业务职责是:调用单个业务操作,而由该业务操作去处理所有的业务细节::
//应用服务只用于协调任务
UserDescriptor userDescriptor =
DomainRegistry
.authenticationService()
.authenticate(aTenantId, aUsername, aPassword);
以上方式是简单的,也是优雅的。客户端只需要获取到一个无状态的 AuthenticationService,然后调用它的authenticate()方法即可。这种方式将所有的认证细节放在领域服务中,而不是应用服务。在需要的情况下,领域服务可以使用任何领域对象来完成操作,包括对密码的加密过程。客户端不需要知道任何认证细节。此时,通用语言也得到了满足,因为我们将所有的领域术语都放在了身份管理这个领域中,而不是一部分放在领域模型中,另一部分放在客户端中。
领域服务方法返回一个UserDescirptor值对象,这是一个很小的对象,并且是安全的。与User相比,它只包含3个关键属性:
public class UserDescriptor implements Serializable {
private String emailAddress;
private TenantId tenantId;
private String username;
public UserDescriptor(
TenantId aTenantId,
String aUsername,
String anEmailAddress) {
...
}
...
}
该UserDescriptor对象可以存放在一次Web会话(Session)中。对于作为客户端的应用服务来说,它可以进一步将该UserDescriptor* 返回给它自己的调用者。