独立接口有必要吗
这里的Authenticationservice接口并没有一个技术上的实现,真的有必要为其创建一个独立接口并将其与实现类分离在不同的层和模块中吗?
没必要。我们只需要创建一个实现类即可,其名字与领域服务的名字相同。
对于领域服务来说,以上的例子同样是可行的。我们甚至会认为这样的例子更合适,因为我们知道不会再有另外的实现类。
但不同的租户可能有不同的安全认证标准,所以产生不同的认证实现类也是有可能的。
然而此时,SaaSOvation的团队成员决定弃用独立接口,而是采用了上例中的实现方法。
给领域服务的实现类命名
常见的命名实现类的方法便是给接口名加上Impl后缀。按这种方法,我们的认证实现类为AuthenticatioinServicelmpl。实现类和接口通常被放在相同包下,这是一种好的做法吗?
如果你釆用这种方式来命名实现类,这往往意味着你根本就不需要一个独立接口。因此,在命名一个实现类时,我们需要仔细地思考。这里的 AuthenticationServicelmpI并不是好的实现类名,而DefaultEncryptionAuthenticationService也不见得能好到哪。
基于这些原因,团队决定去除独立接口,而直接使用Authenticationservice作为实现类。
如果领域服务具有多个实现类,应根据各种实现类的特点进行命名。而这往往又意味着在你的领域中存在一些特定的行为功能。
有人认为采用相似名字命名接口和实现类有助代码浏览和定位。但还有人认为将接口和实现类放在相同包中会使包变很大,这是种糟糕的模块设计,因此偏向于将接口和实现类放在不同包,依赖倒置原则便是如此:将接口Encryptionservice放在领域模型,而将 MD5EncryptionService放在基础设施层。
对于非技术性的领域服务,去除独立接口不会破坏可测试性。因为这些领域服务所依赖的所有接口都可以注入进来或通过服务工厂(Service Factory)进行创建。
非技术性的领域服务,比如计算性的服务等都必须进行正确性测试。
有时,领域服务总是和领域密切相关,并且不会有技术性的实现,或者不会有多个实现,此时采用独立接口便只是一个风格上的问题。
独立接口对于解偶来说是有用处的,此时客户端只需要依赖于接口,而不需要知道具体的实现。但是,如果我们使用了依赖注入或者工厂,即便接口和实现类是合并在一起的,我们依然能达到这样的目的。
以下的DomainRegistry可在客户端和服务实现之间进行解耦,此时的DomainRegistry便是一个服务工厂。
//DomainRegistry在客户端与具体实现之间解耦 UserDescriptor userDescriptor = DomainRegistry .authenticationservice() .authenticate(aTenantld, aUsername, aPassword);
或者,如果你使用的是依赖注入,你也可以得到同样的好处:
public class SomeApplicationService ... { @Autowired private Authenticationservice authenticationservice;
依赖倒置容器(比如Spring)将完成服务实例的注入工作。由于客户端并不负责服务的实例化,它并不知道接口类和实现类是分开的还是合并。
与服务工厂和依赖注入相比,有时他们更倾向于将领域服务作为构造函数参数或者方法参数传入,因为这样的代码拥有很好的可测试性,甚至比依赖注入更加简单。也有人根据实际情况同时采用以上三种方式,并且优先采用基于构造函数的注入方式。本章中有些例子使用了DomainRegistry,但这并不是说我们应该优 先考虑这种方式。互联网上很多源代码例子都倾向于使用构造函数注入,或者直接将领域服务作为方法参数传入。
计算案例
该例子来自于敏捷项目管理上下文。该例子中的领域服务从多个聚合的值对象中计算所需结果。
目前来看,我们没有必要使用独立接口。该领域服务总是采用相同方式进行计算。除非有需求变化,不然我们没必要分离接口和实现。
上很多源代码例子都倾向于使用构造函数注入,或者直 接将领域服务作为方法参数传入。
中有些例子使用了DomainRegistry,但这并不是说我们应该优 先考虑这种方式
釆用领域服务比静态方法更好。此时的领域服务和当前的静态方法完成类似功能:计算并返回一个BusinessPriorityTotals值对象。但是,该领域服务还需要完 成额外的工作,包括找到一个Product中所有未完成的Backlogitem, 然后单独计算它们的BusinessPriority。
BacklogltemRepository用于查找所有未完成的Backlogitem实例。一个未完成的Backlogitem是拥有Planned、Scheduled或Committed状态的Backlogitem,而状态为Done或Removed的Backlogitem则是已完成的。
不推荐将资源库对 Backlogitem的获取放在聚合实例,相反,将其放在领域服务中则是一种好的做法。
有了一个Product下所有未完成的BacklogItem,我们便可对遍历之并计算BusinessPriority总和。总和进一步用于实例化一个 BusinessPriorityTotals,然后返回给客户端。
领域服务不一定非常复杂,即使有时的确会出现这种情况。上面的例子则是非常简单的。请注意,在上面的例子中,绝对不能将业务逻辑放到应用层。即使你认 为这里的for循环非常简单,它依然是业务逻辑。当然,还有另外的原因:
实例化BusinessPriorityTotals时,它的totalValue属性由totalBenefit和 totalPenalty相加所得。这是和领域密切相关的业务逻辑,自然不能泄漏到应用层。你可能会说,可以将totalBenefit和totalPenalty作为两个参数分别传给应用服务。虽然这是一种改进模型的方式,但这也并不意味着将剩下的计算逻辑放在应用层就是合理的。
public class Productservice ... { private BusinessPriorityTotals productBusinessPriority( String aTenantld, String aProductld) { BusinessPriorityTotals productBusinessPriority = DomainRegistry .businessPrioritycalculator() .businessPriorityTotals( new Tenantld(aTenantld), new Productld(aProductld)); return productBusinessPriority; } }
上例中,应用层中的一个私有方法负责获取一个Product的总业务优先级。该方法可能只需要向Productservice的客户端(比如用户界面)提供BusinessPriorityTotals 的部分数据即可。
转换服务
在基础设施层中,更加技术性的领域服务通常是那些用于集成目的的服务。 正是这个原因,我们将与此相关的例子放在了集成限界上下文中,你将看到领域服务接口、实现类、适配器和不同的转换器。
为领域服务创建一个迷你层
有时可能希望在实体和值对象上创建一个领域服务的迷你层。但这样可能导致贫血领域模型。
但对有些系统,为领域服务创建一个不至于导致贫血领域模型的迷你层很值得。这取决于领域模型的特征。对于本书的身份与访问上下文来说,这样的做法是非常有用的。
如果你正工作在这样的领域里,并且你决定为领域服务创建一个迷你层,这样的迷你层不同于应用层中的服务。
应用服务关心的是事务和安全,但这些不该出现在领域服务。
虽然我们不会将业务逻辑放在应用层,但是应用层却可以作为领域服务的客户端。