事实上,团队中的高级开发者并不建议这么做。一个基本原则:应尽量避免在聚合中使用资源库。那么,将businessPriorityTotals()方法声明为静态方法,然后将 Backlogitem集合作为参数传入,如何?
这样,几乎不用对该方法做多少修改,只需传入新参数:
那Product是创建该静态方法的最佳位置吗?
看来要将该方法放在合适的地方并非易事。由于该方法只使用了每个Backlogitem中的值对象,将该方法放在Backlogitem似乎更合适。但这里计算所得的业务价值却属于Product而非Backlogitem,进退维谷!
团队中的高级开发者发话了。他指出:这些问题用一个单一的建模工具即可解决,即领域服务(Domain Service)。
那领域服务是如何工作的?
什么是领域服务
======================================================================
什么不是领域服务?
听到“服务”,自然想到一个远程客户端与某复杂业务系统交互,该场景基本描述了SOA中的一个服务。有多种技术和方法可以实现SOA服务,最终这些服务强调的都是系统层面的
远程过程调用(RPC)或
消息中间件(MQ)
这些技术使得我们可通过服务与分布在不同地方的系统进行业务交互。
以上这些都不是领域服务。
不要将领域服务与应用服务混淆:
应用服务并不会处理业务逻辑
但领域服务恰恰是处理业务逻辑。应用服务是领域模型很自然的客户,也是领域服务的客户。
虽然领域服务中有“服务”这个词,但它并不意味着需要远程的、重量级的事务操作。
何时应该使用领域服务
领域服务到底是什么?当领域中的某个操作或转换过程不是实体或值对象的职责时,此时便应该将该操作放在一个单独的接口,即领域服务。
请确保该领域服务和通用语言是一致的;并且保证它是无状态的。
通常领域模型主要关注特定于某个领域的业务。同样,领域服务也具有相似特点。由于领域服务有可能在单个原子操作中处理多个领域对象,这将增加领域服务的复杂性。
有时,当与另一个限界上下文交互时,领域服务的确需要进行远程操作,但此时重点并非将领域服务作为一个服务提供方,而是将其作为RPC的客户端。
那么何时一个操作不属于实体或值对象?即何时可使用领域服务:
执行一个显著的业务操作过程
对领域对象进行转换
以多个领域对象作为输入进行计算,产生一个值对象结果
计算过程应该具有“显著的业务操作过程”。这也是领域服务很常见的应用场景,它可能需要多个聚合作为输入。
当一个方法不便放在实体或值对象,使用领域服务便是最佳的解决方案。
确定需要领域服务?
========================================================================
请不要过于倾向将一个领域概念建模成领域服务,只有在有必要时才这么做。领域服务不是“银弹”。
过度使用领域服务将导致贫血领域模型,即所有业务逻辑都位于领域服务中,而非实体和值对象。
来看使用领域服务案例:
案例
考虑身份与访问上下文,对一个User进行认证。
为什么领域服务在此时是必要的呢?难道不可以简单地将该认证操作放在实体?从客户角度来看,我们可能会使用以下代码实现认证:
// client finds User and asks it to authenticate itself
boolean authentic = false;
User user = DomainRegistry
.userRepository()
.userWithUsername(aTenantld, aUsername);
if(user != null) {
authentic = user.isAuthentic(aPassword);
}
return authentic;
以上设计至少存在如下问题
他们需要找到一个User,然后再对该User进行密码匹配
这里,我们询问的是一个User "是否被认证”,而没表达“认证”这个过程。在有可能的情况下,我们应尽量使建模术语直接表达出团队成员的交流用语,但还有更糟糕的。
这种建模方式并不能准确表达出团队成员所指的“对User进行认证”的过程。它缺少了 “检查Tenant否处于激活状态”这个前提条件。如果一个User所属的Tenant处于非激活状态,我们便不应该对该User进行认证。
或许可以通过以下方法予以解决:
这种方式的确对Tenant的活跃性做了检查,同时我们也将User#isAuthentic换成Tenant#authenticate
但这种方式也有问题:
Tenant#isActive放在authenticate,但这并不是一个显式的模型。同时这将带来另外一个问题,即此时的Tenant需要知道如何对密码进行操作。
回忆一下该认证过程的另一个需求:
对于以上解决方案,我们似乎给模型带来了太多的问题。对于最后一种方案,我们必须从以下解决办法中选择一种:
在Tenant中处理对密码的加密,然后将加密后的密码传给User。这种方法违背了单一职责原则
由于一个User必须保证对密码的加密,它可能已经知道了一些加密信息。如果这样,我们可在User上创建一个方法,该方法对明文密码进行认证。但这种方式下,认证过程变成了Tenant的门面(Facade),而实际的认证功能全在User。另外,User的认证方法必须声明为protected,以防止外界客户端对认证方法的直接调用
Tenant依赖User对密码进行加密,然后将加密后的密码与原有密码进行匹配
这种方法似乎在对象协作之间增加了额外步骤。此时,Tenant依需要知道认证细节
这样导致问题在于,客户端承载了它本不应该有的职责
以上这些方法都无济于事,同时客户端依然非常复杂。强加在客户端上的职责应该在我们自己的模型中予以处理。只与领域相关的信息决不能泄漏到客户端。即使客户端是一个应用服务,它也不应该负责对身份与访问权限的管理。
客户端需要处理的唯一业务职责是:调用单个业务操作,而由该业务操作去处理所有业务细节:
简单而优雅。客户端只需获取到一个无状态的 Authenticationservice,然后调用authenticate。这种方式将所有认证细节放在领域服务,而非应用服务。在需要的情况下,领域服务可使用任何领域对象完成操作,包括对密码的加密。
客户端无需知道任何认证细节。
通用语言也得到满足,因为我们将所有领域术语都放在了身份管理这个领域,而非一部分放在领域模型,另一部分放在客户端。
领域服务方法返回一个UserDescirptor值对象,这是一个很小的对象,并且是安全的。与User相比,它只包含3个关键属性:
该UserDescriptor对象可存放在一次Web会话(Session)中。对于作为客户端的应用服务,它可进一步将该UserDescriptor返回给它自己的调用者。
如何建模领域服务?
========================================================================
根据创建领域服务的目的,有时对领域服务进行建模是非常简单的。你需要决定你所创建的领域服务是否需要一个独立接口。如果是,你的领域服务接口可能与以下接口相似:
package com.saasovation.identityaccess.domain.model.identity;
该接口和那些与身份相关的聚合(比如Tenant, User和Group)定义在相同的模块中,因为Authenticationservice也是一个与身份相关的概念。当前,我们将所有与身份相关的概念都放在identity模块中。该接口定义本身是简单的,只有一个 authenticate方法。
对于该接口的实现类,我们可以选择性地将其存放在不同地方。如果你正使用
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
依赖倒置原则或六边形架构,那你可能会将这个多少有些技术性的实现类放置在领域模型外。比如,技术实现类可放置在基础设施层的某个模块中。
以下是对该接口的实现:
在对一个User进行认证时:
先根据aTenantld从Tenant的资源库中取出对应的Tenant
如果Tenant存在且处于激活状态,下一步我们将加密传入的明文密码
加密在于我们需要通过加密后的密码获取一个User。在获取一个User时,我们不但需要传aTenantld和username,还需要传入加密后的密码进行匹配(对于两个明文相同的密码,加密后也是相同的)。User的资源库将根据这三个参数来定位一个User。
如果用户提交的aTenantld, username和password都正确,我们将获得相应的User实例。但此时我们依然不能对该User进行认证,我们还需要处理最后一条需求:
即便我们通过资源库找到了一个User,该User也有可能处于未激活。通过向User添加激活功能,Tenants可从另一层面控制对User的认证。因此,认证过程的最后一步即是检查所获取到的User实例是否为null和是否处激活状态。
独立接口有必要吗
=======================================================================
这里的Authenticationservice接口并没有一个技术上的实现,真的有必要为其创建一个独立接口并将其与实现类分离在不同的层和模块中吗?