请确定你是否需要一个领域服务

请不要过于倾向于将一个领域概念建模成领域服务,而是只有在有必要的时候才这么做。一不小心,我们就有可能陷人将领域服务作为“银弹”的陷进。过度地使用领域服务将导致贫血领域模型[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需要知道如何对密码进行

操作。回忆一下该认证过程的另一个需求:

  • 必须对密码进行加密,并且不能使用明文密码。

对于以上解决方案,我们似乎给模型带来了太多的问题。对于最后一种方案,我们必须从以下四种解决办法中选择一种:

  1. 在Tenant中处理对密码的加密,然后将加密后的密码传给User。这种方法违背了单一职责原则[Martin,SRP]。
  2. 由于一个User必须保证对密码的加密,它可能已经知道了一些加密信息。如果是这样,我们可以在User上创建一个方法,该方法对明文密码进行认证。但是,在这种方式下,认证过程变成了Tenant上的门面(Facade),而实际的认证功能全在UserJi。另外,User上的认证方法必须声明为protected,以防止外界客户端对认证方法的直接调用。
  3. Tenant依赖于User对密码进行加密,然后将加密后的密码与原有密码进行匹配。这种方法似乎在对象协作之间增加了额外的步骤。此时,Tenant依然需要知道认证细节。
  4. 让客户端对密码进行加密,然后将其传给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* 返回给它自己的调用者。

results matching ""

    No results matching ""