戏说领域驱动设计(十七)——实体实战

【戏说领域驱动设计(十七)——实体实战】上一节中讲了实体的一些概念,作为DDD中最为复杂的组件,想用好了还需要在实践中慢慢去摸索,都是摸爬滚打过来的。本章着重演示一些实体相关的代码,通过建立一个基类和通用方法,能让您在开发过程中少写一些重复的代码同时也减少在使用第三方开源框架时的学习成本。此外,是从0写代码,不需要付出太多的精力便可以加深自身对理论的理解。友情提示一下,您在看的同时也需要回忆一下前面文章中所说的各类规则、限制,理论与实践相互印证才能更高效。其实在业务系统开发过程中很少会直接从零写实体的,多多少少也得有一些基类供使用,毕竟有很多东西是通用的,建一个实体就重写一次您不累吗?本章我会从一些基础的内容开始展示在不用任何架构的情况下如果实践DDD。代码仅供参考,每个人的实现方式都会不一样,了解思路即可。
一、领域模型基类 领域模型基类是实体和值对象共同的父类,虽然实体和值对象作用不一样但都属于领域模型。这个基类无任何属性,只是起到了占位符的作用。后面有些功能比如“领域模型验证工具”要求待验证的目标应该是领域模型。具体代码如下。

/** * 领域模型基类 */ public abstract class DomainModel extends ValidatableBase {/** * 初始化当前状态 */ public void initializeForNewCreation() {} }

方法“initializeForNewCreation”用于初始化新建的对象,比如在new对象后进行一些属性的默认值设置,现实中有些场景可能还需要特殊的初始化方式,一般会放到领域对象工厂中完成。您可能会注意到,领域模型从类“ValidatableBase”继承,这样做的目的表示领域模型是可被验证的。比如领域模型持久化前或从反序列后需要进行对象合法性的验证,而我又不想在每次对属性赋值后都判断值的合法性,好的方式是进行统一的验证并将不合法的内容统一抛出去。对象内部提供验证方法我称之为“内验”,内验的目标是对象的属性或属性组合,也就是只验证模型本身是否合法,不验证外部条件。
有人认为把对象验证的方法放到领域模型中会造成模型的责任变重,所以会建立专门用于验证的类或服务。我个人觉得一个对象属性是否合法是一种业务规则,应由对象自已责任,由其自己验证可产生较好的内聚性。就和人生病一样,自己最了解哪里最不爽。此外,我所用的“内验”并未让对象自己执行验证(虽然你也可以进行手动的调用)而是在其中设置验证规则并由专门的验证服务负责执行验证逻辑。下面代码演示了如何在领域模型中嵌入验证规则,需要注意的是本章重点并不在验证上面,这方面内容会启动一个新的章节做专门讲解。
/** * 可验证对象的基类 */ public abstract class ValidatableBase implements Validatable { …… protected void addRule(RuleManager ruleManager){}

final public ParameterValidationResult validate() {
……
}
…… }public class DeploymentApprover extends ApproverBase {
private PhaseType targetPhase; …… @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("targetPhase", this.targetPhase, OperationMessages.INVALID_ROLE_TYPE)); ruleManager.addRule(new NotEqualsRule("targetPhase", this.targetPhase, PhaseType.UNKNOWN, OperationMessages.INVALID_ROLE_TYPE)); } …… }

上述代码中的“addRule”方法定义于父类“ValidatableBase”中,用于为领域模型增加验证规则,比如属性“status”的值不能是“null”和“PhaseType.UNKNOWN”。这种只加规则不验证的方式实际上有点规约模式(Specification )的味道,算是一个简化版。
二、实体类型基类 实体类型也算是一种领域模型,所以我们就可以在既有的领域模型的基础上设计实体类型的基类,所有的业务实体都从这个基类继承,请参看如下代码。
public abstract class EntityModel extends DomainModel implements Versionable { //ID private TID id; //版本信息,用于控制并发 private int version; //创建日期 private LocalDateTime createdDate = LocalDateTime.now(); //变更日期 private LocalDateTime updatedDate = LocalDateTime.now(); //状态 privateStatus status =Status.ACTIVE; protected EntityModel(TID id) { this(id, null, null); }protected EntityModel(TID id, LocalDateTime createdDate, LocalDateTime updatedDate) { this(id,Status.ACTIVE, 0, createdDate, updatedDate); }protected EntityModel(TID id,Status status, LocalDateTime createdDate, LocalDateTime updatedDate) { this(id, status, 0, createdDate, updatedDate); }protected EntityModel(TID id,Status status, int version, LocalDateTime createdDate, LocalDateTime updatedDate) { this.id = id; this.version = version; this.initializeForNewCreation(); if (status != null && status !=Status.UNKNOWN) { this.status = status; } if (createdDate != null) { this.createdDate = createdDate; } if (updatedDate != null) { this.updatedDate = updatedDate; } }/** * 当前对象置无效 */ public void disable() throws InvalidOperationException { this.status =Status.INACTIVE; this.updatedDate = LocalDateTime.now(); } @Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("id", this.id, OperationMessages.INVALID_ID)); }@Override public boolean equals(Object object){ if(object == null){ return false; } if(!(object instanceof EntityModel)){ return false; } if(object == this){ return true; } return this.id.compareTo(((EntityModel)object).getId()) == 0; }@Override public int hashCode(){ return this.id.hashCode(); }/** * 获取版本信息。 * @return 版本信息 */ @Override public int getVersion() { return this.version; } }

上面的代码作为演示用没有把所有的方法列出来,您需要了解和关注其中一些重要的概念。案例中引入了一个新的接口“Versionable”,这个接口用于为实体模型增加乐观锁支撑。通过在类中引入属性“version”,每次对实体进行变更时此字段加1。涉及乐观锁的概念及使用方式可参看网络上其它文章。在DDD中,使用乐观锁可以说是一种最起码的要求且并不需要付出太多的精力,还是十分推荐的。
第二个重点内容是标识属性“id”,每一个实体必须有一个标识属性用于对其生命周期进行跟踪。案例中使用了泛型表示实体的ID类型,不过仍然要求ID是可以比较的(Comparable)。您可以通过重写方法“equals”及“hashCode”来实现实体间的比较,这两个方法一般都是成对出现,具体原因可自行参考相关文章。需要注意的是“equals”的实现,两个对象相等不看属性只看ID,所以代码实现的时候只对ID做比较。针对ID的设计其实还有一个方式,就是设计一个专门表示ID的类,将ID的操作如等价判断直接放在类中,这样可以让ID的设计更加优雅也能减少实体对象的责任,请看如下代码。
public class Identity extends ValueModel { private TID id; @Override public boolean equals(Object obj) { if(obj == null){ return false; } if(!(obj instanceof Identity)){ return false; } if(obj == this){ return true; } return this.id.compareTo(((Identity)obj).getId()) == 0; } }

public abstract class EntityModel { private Identity id; protected EntityModel(Identity id) { this.id = id; }@Override public boolean equals(Object obj) { if(obj == null || !(obj instanceof EntityModel)){ return false; } return this.getId().equals(((EntityModel) obj).getId()); } }

上述的案例在ID设计方面要比第一个版本漂亮得多,也显得更加专业。实际在做面向对象编程的时候,将责任细化到各个小一点的对象中是一种非常常见的情况,这也是为什么我在前面说使用OOP的时候成本比较高。单一责任的目的倒是达到了,不过出现一堆稀碎的对象,组装起来也挺费劲的。
我们再回到实体模型的设计上,您会发现我严格遵循了一些原则:1)使用构造函数的方式来实现对所有对象属性的赋值,虽然没有在赋值的时候对属性的是否合法进行保障,但由于使用了前面所说的“内验”的方式对对象进行验证,也就是对象工厂在创建实体后调用其验证方法“validate()”,也可以保障实体的合法性。实际上,在我写文本篇文章时进行了代码的走查,才发现基类“EntityModel”中未对ID的正确性进行验证又没有限定对象必须使用工厂创建,而是把验证放到了持久化前的阶段,这样还是有一定的风险的。那么文章结束后我肯定需要对代码进行调整的。创建实体的原则您需要格外注意:不论使用工厂还是构造函数,一旦业务对象被成功创建就应该是合法的,不需要也不应该再调用其它方法进行补偿(比如对象创建后手动调用某个初始化方法);2)实体中引入了一些通用属性比如“status”,表示对象是活越的还是已经被废了,在数据的角度看就是数据是否被逻辑删除。一般来说,我们不会对对象做物理删除,实体对象只要被创建且进行了持久化,就表示其曾经来到过这个世界上,只是因一些事件他已经不活越了,所以不应该将其直接干掉。
三、业务实体的设计 上面通过代码展示了实体基类的设计方式,在此基础上就可以进行业务实体模型的设计。下面展示了工作流业务模型的代码片段,其设计方式仍然遵循我们谈及的规范。如果您是一个有强迫症的设计师,可能会对“forward”方法比较纠结。通常情况下,跨领域模型的操作应当由“领域服务”来完成,不过我们这里并没有采用这种模式。因为此段代码是工作流的基类,往大了说算是工作流框架的一部分。在项目中引入“由领域服务完成跨领域模型的业务操作”是一个很好的规范,值得遵守。
public abstract class WorkFlowInstanceBase extends EntityModel { public static final long EMPTY_WORK_NODE = -1; private Long templateId; //工作流模板 private Creator creator; //创建人 private String request; //请求信息 private String title; //标题 private Long currentWorkNodeId = EMPTY_WORK_NODE; //当前处理节点 protected WorkFlowInstanceBase(Long id, String title, DataStatus dataStatus, LocalDateTime createdDate, LocalDateTime updatedDate, Long templateId, Creator creator, String request, Long currentWorkNodeId) { super(id, dataStatus, createdDate, updatedDate); this.title = title; this.templateId = templateId; this.creator = creator; this.request = request; this.currentWorkNodeId = currentWorkNodeId; }/** * 转向下一个处理节点 * @param comment 备注 * @param template 模板 * @return 处理记录 */ protected ProcessRecord forward(String comment, WorkFlowTemplateBase template) throws InvalidOperationException { if (StringUtils.isEmpty(comment)) { throw new InvalidOperationException(OperationMessages.INVALID_COMMENT); }return this.forwardCore(comment, template, currentWorkNode); }@Override protected void addRule(RuleManager ruleManager) { super.addRule(ruleManager); ruleManager.addRule(new ObjectNotNullRule("currentWorkNodeId", this.currentWorkNodeId, OperationMessages.INVALID_CURRENT_WORK_NODE)); ruleManager.addRule(new ObjectNotNullRule("templateId", this.templateId, OperationMessages.INVALID_TEMPLATE)); ruleManager.addRule(new ObjectNotNullRule("creator", this.creator, OperationMessages.INVALID_CREATOR_INFO)); ruleManager.addRule(new EmbeddedObjectRule("creator", this.creator)); ruleManager.addRule(new StringNotNullOrEmptyRule("request", this.request, OperationMessages.INVALID_REQUEST)); ruleManager.addRule(new StringNotNullOrEmptyRule("title", this.title, OperationMessages.INVALID_TITLE)); } }

总结 本章中所示的代码相对简单明了,没有那么多的花里胡哨,别看东西少但足够在真实的项目中使用,类似于事件溯源这种,个觉得真正需要的场景并不是很多,所以也没有加到基类中来。我见过一些个人开发的框架,把代码设计的特别复杂,可以说是包罗万象。但其价值有几何,估计也是仁者见仁、智者见智罢了。另外呢,个人建议在实践DDD的时候,从这种简单的途径开始即可,自己写一点东西能帮助您在实战中多积累一些经验。类似AXON这种大型框架,您别看他东西多,其实并没有脱离DDD战术中所说的那点事情。
下一章我们讨论内验,较早之前我写过验证相关的文章,不过在决定开启DDD系列后就将其屏蔽掉了,没头没尾的不太好。

    推荐阅读