一、领域服务
当操作不适合放在聚合和值对象上时,最好的方式就是使用领域服务。领域服务是一个无状态的操作,一个领域服务有可能操作多个领域对象,它用于实现特定于某个领域的任务。领域服务需要处理逻辑,不建议做为soap接口对外直接暴露。一般在下列情况下可以抽取服务,否则还是放在聚合或实体中会比较合适:
- 执行一个显著的业务操作过程;
- 对领域对象进行转换;
- 以多个领域对象作为输入进行计算,输出一个值对象;
1.1、领域服务设计
值得注意的是,过度的使用领域服务会导致失血的领域模型,即所有的业务和逻辑都位于领域服务中。java实现时领域服务表现为一个接口或一个服务类,使用类还是接口取决于业务,如果业务不存在多态直接用实现类可能会更方便一些。
虽然接口有利于概念定义和解耦,但如果采用依赖注入、工厂,把领域服务作为参数传递时这两者就没有太大的区别了,这时可以从测试是否方便的角度来考虑采用哪种落地实现。领域服务一般会有以下几种实现方案:
- 服务工厂,不建议使用抽象工厂,原因是不利于表述业务;
- 接口实现,接口和实现分离;
- 构造函数注入实现,即直接用实现类,最方便测试;
实现领域服务时需要注意三点:1、消息去重;2、幂等操作;3、顺序执行;
1.2、领域服务测试
领域服务是针对特定业务的,所以建议采用单元测试的方式来验证,同时注意封装性即可,如下例;
public void testAuthenticationSuccess() throws Exception {
User user = this.userAggregate();
DomainRegistry
.userRepository()
.add(user);
UserDescriptor userDescriptor =
DomainRegistry.authenticationService() .authenticate(
user.tenantId(),
user.username(),
FIXTURE_PASSWORD);
assertNotNull(userDescriptor);
assertFalse(userDescriptor.isNullDescriptor());
assertEquals(userDescriptor.tenantId(), user.tenantId());
assertEquals(userDescriptor.username(), user.username());
assertEquals(userDescriptor.emailAddress(), user.person().emailAddress().address());
}
领域服务应用服务
应用服务负责提问题,主要职责是处于安全和事务,领域服务负责回答应用服务的问题,主要职责是处理业务逻辑
二、领域事件
将领域中所发生的活动建模成一系列的离散事件,每个事件都用领域对象来表示,它表示领域中所发生的事情,这就是领域事件。领域事件是领域模型的的重要组成部分,用来维护事务一致性,也可支持聚合(聚合设计的原则之一就是在单个事务中,只允许对一个聚合实例进行修改,关联的聚合修改必须在单独的事务中完成)。
领域事件可发生在同一个限界上下文中,也可发生在多个限界上下文中,在同一个上下文中可以采用类似springEvent的方式实现,在多个限界上下文中多数采用基于消息(mq)的方式实现。领域事件设计方案如下:
领域事件可以由聚合、领域服务甚至是用户发布,典形的适用场景如下:
- 聚合发出:比如在构造函数中创建后发出;
- 领域服务:适合把把部分业务过程封装成领域服务时,这时可以采用事件通知的方式触发;
- 用户发出:由用户从操作端发布,由领域服务接收,创建再发布;
2.1、领域事件例子
为了更好的理解领域事件,下面讲述一个由聚合发布领域事件的例子,使大家有一个理性的感知:
事件模型声明
public interface DomainEvent {
public int eventVersion();
public Date occurredOn();
}
实例化的特定事件
/*规则1:命令要反映出执行成功之后所发生的事情###BacklogItem提交给sprint完毕后*/
class CommittedToSprinted implements DomainEvent{
private EventId eventId;//事件标识
/*规则2:必要的属性*/
private Date occurredOn;//事件发生的时间
private BacklogItemId BacklogItemid//事件发起方
private SprintId committedToSprintId;//事件参与方
/*事件参数*/
private TenantId tenantId//租房ID,限定上下文
}
发布事件
//发布事件
public class Product extends Entity {
private Set<ProductBacklogItem> backlogItems;
public Product(TenantId aTenantId) {
this();
DomainEventPublisher.instance().publish(new ProductCreated(
this.tenantId(),
this.productId())
);
}
}
2.2、领域事件架构设计
笔者并不建议在系统中采用领域事件,原因是其非常复杂且难以控制,本节中笔记不会详细展开,只从宏观上带大家对事件有个整体的认识。
- 领域命令:由用户触发,是一种请求,可被拒绝;
- 领域事件:一般由系统或定时系统触发,是一种事实,不可被拒绝;
- 领域快照:减少查询压力;
- 异步事件:用于填充快照数据;
同一上下文中发布领域事件的设计
在同一领域模型中需要注意,注册要先于发布,是在领域模型的方法流中进行注册的,即要在同一线程中。这也就是一般用应用服务来注册,而不用领域服务来注册的原因,即越靠前越好;
领域事件事件存储的设计
事件存储是一个可选的模块,如果不需要,则不需要设计。如果设计了事件存储那么就需要两步:存储+转发。事件存储主要是做如下事情:
- 在不同上下文中集成,维护一致性;
- 检查由模型的命令方法所产生的所有结果的历史记录,跟踪BUG;
- 使用事件存储中的数据来进行业务预测和分析;
- 当从资源库中获取一个聚合实例时,使用事件来重建此聚合实例;
- 撤销对聚合的操作,添加补丁来修复系统中的bug;
2.3、DDD中的事件源设计
事件源就通过事件来表示一个聚合的完整状态,这里的事件是自聚合创建以来的一系列事件。通过按照产生时的顺序重放这些事件,我们可以重建聚合的状态。这些用于重建聚合状态的事件位于同一个事件流中,事件源确保每次聚合改变的原因都不会丢失。这种设计的优缺点如下:
优点:
- 源于同一个事件源,且所有事件流都将被持久化到事件存储中;可用于追踪、回放、下载移植;
- 只能向流中追加到尾部,追加后聚合状态将会进一步改变;且状态变化不可回退,这是基于事件是已经发生过的事实的约束设计的;
- 各个事件彼此分离,一般采用根实体的唯一标识;
缺点:
- 需要对业务领域非常了解,同时模型又非常复杂才适合;
- 需要相应的工具以及一致的知识体系支持;比如事件序列器工具、协议生成工具(事件定义契约、事件发布契约)等;
- 必须采用CQRS架构,学习成本和实际成本会很高;
- 需要不同的开发、构建、部署方案;
三、模块
模块并不是领域的一部分,但却有必要把模块名称做为通用语言在团队范围内同步,模块表示了一个命名的容器,用于存放领域中内聚在一起的类以达到更粗粒度松耦合的目的。模块是一种概念,它位于限界上下文和模型之间。
模块的概念有可能对软件开发人员产生引导作用,把模块与工程结构以及工程抽包甚至应用部署联系起来,这种概念的混淆需要纠正,因为模块关注的是领域模型的设计是否合理,而工程结构需要的是在代码可读性和维护成本间做出权衡。
模块的设计原则
- 模块应该和领域概念保持协调一致:通常对于一个或一组内聚的聚合来说,我们都可以相应地创建一个模块;
- 根据通用语言来命名模块;
- 不建议按功能把相同功能的类放在一起,比如工厂放在工厂包中,领域服务放在单独中;这无形中给我们的层次调用增加了限制;
- 设计松耦合的模块,模块应该是独立和可插拔的,必要时可以使用版本设计;
- 杜绝循环依赖,但同层次调用有时认为是允许的;
- 不要将模块设计成一个静态的概念,最好与模型中的对象一起进行建模,随着模型的演进而演进;