有时,它不见得是件东西 ——Eric Evans
领域中的服务标识一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。
什么是领域服务(首先,什么不是领域服务)
请不要将领域服务与应用服务混杂在一起了。在应用服务中,我们并不会处理业务逻辑,但是领域服务却恰恰是处理业务逻辑的。简单来讲,应用服务是领域模型很自然的客户方,进而也是领域服务的客户方。
通常来说,领域模型主要关注与特定于某个领域的业务,同样,领域服务也具有相似的特点。
那么,在什么情况下,一个操作不属于实体或者值对象呢?这里罗列了一下几点,你可以使用领域服务来:
- 执行一个显著的业务操作过程。
- 对领域对象进行转换
- 以多个领域对象作为输入进行计算,结果产生一个值对象
需要明确的是,对于最后一点中的计算过程,它应该具有“显著的业务操作过程”的特点。这也是领域服务很常见的应用场景。请确保领域服务是无状态的,并且明确地表达限界上下文的通用语言。
请确定你是否需要一个领域服务
请不要过于倾向将一个领域概念建模成领域服务,而是只有在有必要的时候才这么做。我们就有可能陷入将领域服务作为“银弹”的陷阱。过度使用领域服务将导致贫血模型,即所有的业务逻辑都位于领域服务中,而不是实体和值对象中。
让我们看一个需要建立领域服务的例子。
系统必须对User进行认证,并且只有当Tenant处于激活状态才能对User进行认证
我们来看看为什么领域服务在此时是必要的。从客户的角度来看,我们可能会使用一下代码来实现认证过程。
boolean authentic = false;
User user = DomainRegistry.userRepository().userWithUsername(aTanantId,aUsername);
if(user != null) {
authentic = user.isAuthentic(aPassword);
}
return authentic;
对于以上设计,我们认为至少存在两个问题。
- 首先,客户端需要知道某些认证细节,它们需要找到一个User,然后再对该User进行密码匹配。这种方法也不能显示地表达通用语言。这里我们询问的是一个User“是否被认证了”,而没有表达出“认证”这个过程。
- 这种建模方式并不能准确地表达出团队成员所指的“对User进行认证”的过程。它缺少了“检查Tenant是否处于激活状态”这个前提条件
boolean autoentic = false;
Tenant tenant = DomainRegistry.tenantRepository().tenantOfId(aTenantId);
if (tenant != null && tenant.isActive()) {
User user = DomainRegistry.userRepositry().userWithUsername(aTenantId, aUsername);
}
if (user != null) {
authentic = tenant.authenticate(user,aPassword);
}
return autoentic;
同时,这将带来另外一个问题,即此时的Tenant需要知道如何对密码进行操作。我们必须从下下四种解决办法中选择一种:
- 在Tenant中处理对密码的加密,然后对加密后的密码传给User。这种方法违背了单一职责原则。
- 由于一个User必须保证对密码的加密,它可能已经知道了一些加密信息。如果是这样,我们可以在User上创建一个方法,该方法对明文密码进行认证。但是这种方式,认证过程变成了Tenant上的门面(Facade),而实际的认证功能全在User上。另外User上的认证方法必须声明为protected,以防止外界客户端对认证方法的直接调用。
- Tenant依赖于User对密码进行加密,然后将加密后的密码于原有密码进行匹配。这种方法似乎在对象协作之间增加了额外的步骤。此时,Tenant依然需要知道认证细节。
- 让客户端对密码加密,然后将其传给Tenant。这样导致,客户端承载了它本不应该有的职责。
以上方法都无济于事。
UserDescriptor userDescriptor = DomainRegistry.authenticactionService().authenticate(aTenantId,aUsername,aPassword);
以上方法是简单的,也是优雅的。
建立领域服务
根据创建领域服务的目的,有时对领域服务进行建模是非常简单的。你需要决定所创建的领域服务是否需要一个独立接口。
public interface AuthenticationService {
public UserDescriptor authenticate(TenantId aTenantId, String aUsername, String aPassword);
}
对于接口的实现类,我们可以选择性的将其存放在不同的地方。如果你正在使用依赖倒置原则或六边形结构,那么你可能会将这个多少有些技术性的实现类放置在领域模型之外。
有时,领域服务总是和领域密切相关,并且不会有技术性的实现,或者不会有多个实现,此时采用独立接口便只是一个风格上的问题。Folwer在【Folwer,P of EAA】中说,独立接口对于解耦来说是有用处的,此时客户端只需要依赖于接口,而不需要知道具体的实现。
但是,如果我们使用依赖注入和工厂,即便接口和实现类是合并在一起的,我们依然能达到这样的目的。换句话说,一下的DomainRegistry可以在客户端和服务端之间进行解耦,此时的DomainRegistry便是一个服务工厂。
// DomainRegistry在客户端与具体实现之间解耦
UserDescriptor userDescriptor = DomainRegistry.authenticationService().authenticate(aTenantId, aUsername, aPassword);
或者,如果你使用的是依赖注入,你也可以得到同样的好处:
public class SomeApplicationService... {
@Autowrited
private AuthenticationService authenticationService;
...
}