《实现领域驱动设计》读书笔记(6) - 战术建模之领域服务
系列大纲: 《实现领域驱动设计》读书笔记
本文大纲:
前言
领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在实体和值对象上时,最好的方式便是使用领域服务了。有时我们倾向于使用聚合根上的静态方法来实现这些操作,但是在 DDD 中,这是一种代码异味。
什么是领域服务
虽然领域服务中有 “服务” 这个词,但它并不意味着作为远程的,重量级的事务操作的提供方。当领域中的某个操作过程或转换过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。参考以下几点来对领域模型建模:
- 执行一个显著的业务操作过程
- 对领域对象进行转换
- 以多个领域对象作为输入进行计算,结果产生一个值对象。
以上第三点提到的 “计算”,也应该具有 “显著的业务操作过程” 的特点。请确保领域服务是无状态的,并且能够明确的表达限界上下文中的「通用语言」。
过度得使用领域服务将导致贫血领域模型,即所有的业务逻辑都位于领域服务中,而不是实体和值对象中。以下的例子是一个使用领域服务的情况,假设我们有以下需求:
- 系统必须对 User 进行认证(authenticate),并且只有当 Tenant 处于激活状态时才能对 User 进行认证。
- 密码必须经过加密,且不能使用明文密码
此时,认证细节不属于 Tenant 或 User 的职责,应该创建一个专门处理认证逻辑的领域服务,客户端伪代码如下:
1 | var authenticationService = DomainRegistry.AuthenticationService(); |
客户端只需获取到一个无状态的 AuthenticationService
,然后调用它的 Authenticate 方法即可。与认证有关的所有实现细节放在领域服务中,在需要的情况下,领域服务可以使用任何领域对象来完成操作,包括对密码的加密过程。客户端不需要知道任何认证细节。该方法返回一个 UserDescriptor 值对象,这是一个很小的对象,并且是安全的。
而调用这段代码的客户方,在多数情况下为「应用服务」,它可以进一步将该 UserDescriptor 对象返回给它自己的调用者,由此可见领域服务和应用服务的区别。
独立的接口和命名实践
如果该领域服务可能有多种实现,那么应该为其定义单独的接口,该接口应该与身份相关聚合(比如 Tenant,User 和 Group)定义在相同的「模块」中,因为 AuthenticationService
也是一个与身份相关的概念。而该接口的实现类——如果正在使用「依赖倒置原则」或「六边形架构」,可以放置在基础设施层的某个模块中。
在 C# 中通常以 I 字符开头来表示接口,此处的接口名称为 IAuthenticationService,但如果这里将实现类命名为 AuthentionService 或 DefaultAuthenticationService,这通常意味着根本就不需要一个接口,如果领域服务有多个实现类,那么应该根据各种实现类的特点进行命名,这也意味着在领域中存在一些特定的功能。对于非技术性的领域服务来说,去除接口是不会破坏可测试性的,因为该服务依赖的所有接口都可以注入进来。
依据笔者的理解,作者此处是想说明,接口很容易遭到滥用,很多模块将接口和其默认实现定义在同一个包中,这通常可以由一个单一的实现类来代替。