解构领域驱动设计(三):领域驱动设计
在上一部分,分层架构的目的是为了将业务规则剥离出来在单独的领域层中进行实现。再回顾一下领域驱动设计的分层中应用层代码的实现。 @Override public void pay(int orderId,float amount) { DesignerOrder order = designerOrderRepository.selectByKey(orderId); // 领域对象的加载 if (order == null) { AppException.throwAppException(AppExceptionMessage.DESIGNER_ORDER_NOT_EXIST_CODE,AppExceptionMessage.DESIGNER_ORDER_NOT_EXIST,orderId); } order.pay(amount); // 领域对象业务规则实现 designerOrderRepository.update(order); // 领域对象状态持久化 } ? 所有的业务规则都抽象到领域对象,比如“order.pay(amount)”抽象了付款的业务规则。领域对象由状态(对象的字段、属性)和操作(对象的方法)构成,领域对象的操作用于实现业务规则,业务规则执行完成后更改领域对象的状态。领域对象的持久化交给了基础设施层,这里,Repository目的是持久化领域对象状态。 领域驱动设计,即领域模型驱动程序设计,它的核心是保证系统的实现与实际的业务规则一致,完整实现了领域模型。它包含了两个部分:领域模型、领域模型的编程实现。 在软件设计和实现过程中要充分利用领域模型,设计过程中,领域模型作为与业务专家的沟通语言;实现过程中,领域模型作为与开发人员沟通的语言。领域模型在软件生命周期过程作为通用语言。 1 领域模型 领域建模(这里不重点介绍如何建模)方法论产出领域模型。我们可以使用UML建模,使用最简单、最容易理解的名词-形容词-动词法对领域知识进行建模,使用该模型作为与业务、技术团队沟通的通用语言。 在名词-形容词-动词法建模方法中,领域知识中的名词一般对应模型、形容词对应模型属性、动词对应模型方法。模型之间的关系有:组合、聚合、关联、依赖,四者关系由强到弱。 依赖(Dependency)关系是类与类之间的联接。依赖关系表示一个类依赖于另一个类的定义。一般而言,依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。? 关联(Association)关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。关联可以是双向的,也可以是单向的。在Java语言中,关联关系一般使用成员变量来实现。? 聚合(Aggregation)?关系是关联关系的一种,是强的关联关系。聚合是整体和个体之间的关系。例如,汽车类与引擎类、轮胎类,以及其它的零件类之间的关系便整体和个体的关系。与关联关系一样,聚合关系也是通过实例变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分。? 组合(Composition)?关系是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分对象的生命周期,组合关系是不能共享的。代表整体的对象需要负责保持部分对象和存活,在一些情况下将负责代表部分的对象湮灭掉。代表整体的对象可以将代表部分的对象传递给另一个对象,由后者负责此对象的生命周期。换言之,代表部分的对象在每一个时刻只能与一个对象发生组合关系,由后者排他地负责生命周期。部分和整体的生命周期一样。? 简而言之,组合关系表示部分与整体关系,部分不能单独存在;聚合关系表示稍弱的部分与整体关系,部分可以单独存在;关联关系是一个模型和另一个模型的联接,比如一个订单有一个顾客而一个顾客有多个订单;依赖是最弱的关系,表示一个模型的实现使用到另一个模型的功能。 举个例子,我们与业务专家沟通,梳理了如下业务知识,然后我们使用名词-形容词-动词法来进行建模。 ===================== 在这里我们可以梳理出来的名词有:客户、设计师订单、设计师、订单交付进度与交付节点、退款订单。 因此,我们通过使用名词-形容词-动词法构建的模型图如下所示。 这里,模型有:客户Customer,设计师Designer,设计师订单DesignerOrder,退款单RefundOrder,设计进度DesigningProgressReport,设计进度节点DesigningProgressNode。模型中组合关系为:设计进度DesigningProgressReport,设计进度节点DesigningProgressNode;其它模型之间的关系为关联关系。 这个模型就作为软件开发和维护过程的通用语言。接下来,我们将介绍如何来实现领域模型。 2 领域模型实现 在上一节,我们介绍了通过领域建模来构建了领域模型。接下来我们要介绍如何实现模型驱动程序设计,即我们如何通过代码来实现领域模型对应的业务逻辑。领域模型的实现代码在领域层,它完整实现了领域模型的内部结构和模型之间的关系。 领域模型的实现代码由以下几个部分构成: 2.1 领域模型关系的实现 聚合、组合、关联关系在实现上的表现基本上是一个类(或者类的标识)作为另一个类的属性;而依赖关系则是一个类作为另一个类在方法的实现上的参数、变量,为另一个类提供功能实现。 下面我们简单看一下如何通过编码来实现类关联关系,比如在模型上客户和设计师订单是关联关系,一个客户可以有多个设计师订单,但是每一个设计师订单只能有一个客户和一个设计师并且最多只有一个退款订单。 (1)聚合、组合、关联 public class DesignerOrder implements Entity<DesignerOrder> { private int id; private int designerId; private DesigningProgressReport progressReport; …… public Designer getDesigner() { return designerRepository.getDesignerById(this.designerId); } public DesigningProgressReport getProgressReport() { return this.progressReport; } …… } ? (2)依赖 public class DesignerOrder implements Entity<DesignerOrder> { public void pay(float amount) { Assert.isTrue(amount > 0,"The amount must be bigger than 0."); if (!DesignerOrderWorkflowService.canChangeState(state,DesignerOrderState.PAID)) { BusinessException.throwException(DomainExceptionMessage.PAYMENT_NOT_IN_READY_STATE_CODE,DomainExceptionMessage.PAYMENT_NOT_IN_READY_STATE,this.id,this.state); } if (Math.abs(amount - this.expectedAmount) > 0.01) { BusinessException.throwException(DomainExceptionMessage.PAYMENT_NOT_MATCHED_CODE,DomainExceptionMessage.PAYMENT_NOT_MATCHED,this.expectedAmount,amount); } this.state = DesignerOrderWorkflowService.changeState(this.id,state,DesignerOrderState.PAID); this.actualPaidAmount = amount; // 付款完成后,自动启动进度跟踪 this.progressReport.startup(); } } ? 2.2 领域模型的实现 领域模型在实现上表现为两类:(1)实体(Entity):这个领域模型有特定的标识,但是其内部状态会随着一序列的事件(对应业务规则的执行)发生变化,我们把这类模型的实现称为实体;(2)值对象(Value Object):这个领域模型由属性来定义,实例创建后不会发生变更,变更也意味着重新创建一个实例,我们把这类模型的实现称为值对象。 (1)实体 实体的属性和操作,对应着模型的状态和状态的变更,他们与模型的定义使一致的。 @Data @EqualsAndHashCode(of = {"id"}) public class DesignerOrder implements Entity<DesignerOrder> { private int id; private DesignerOrderState state; private int customerId; private int designerId; private float area; private float expectedAmount; private int estimatedDays; private DesigningProgressReport progressReport; private String abortCause; private float actualPaidAmount; private int feedbackStar; private String feedbackDescription; private Date createdTime; private Date updatedTime; @Override public boolean sameIdentityAs(DesignerOrder other) { return this.equals(other); } public void measure(float area) { Assert.isTrue(area > 0,"The area must be bigger than 0."); this.state = DesignerOrderWorkflowService.changeState(this.id,DesignerOrderState.MEASURED); this.area = area; } public void quote(float amount,int[] estimatedDaysList) { Assert.isTrue(amount > 0,"The price must be bigger than 0."); this.assertEstimatedDaysList(estimatedDaysList); this.state = DesignerOrderWorkflowService.changeState(this.id,DesignerOrderState.QUOTED); this.expectedAmount = amount; this.progressReport = DesigningProgressReportFactory.newReport(this,estimatedDaysList); this.estimatedDays = this.progressReport.getEstimatedCompletionDays(); } private void assertEstimatedDaysList(int[] estimatedDaysList) { if (null == estimatedDaysList || estimatedDaysList.length != 4) { throw new IllegalArgumentException("The size of estimatedDaysList must be 4."); } for (int days : estimatedDaysList) { if (days <= 0) { throw new IllegalArgumentException("Each element of estimatedDaysList must be bigger than 0."); } } } public void pay(float amount) { Assert.isTrue(amount > 0,DesignerOrderState.PAID); this.actualPaidAmount = amount; // 付款完成后,自动启动进度跟踪 this.progressReport.startup(); } public RefundOrder refund(String cause) { this.assertCanRefund(); this.state = DesignerOrderWorkflowService.changeState(this.id,DesignerOrderState.REFUND); return RefundOrderFactory.newRefundOrder(this,cause); } } ? DDD对于实体有一段重要描述:当一个对象由其标识而不是属性区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事务”。 (2)值对象 public class RouteSpecification extends AbstractSpecification<Itinerary> implements ValueObject<RouteSpecification> { private Location origin; private Location destination; private Date arrivalDeadline; public RouteSpecification(final Location origin,final Location destination,final Date arrivalDeadline) { Validate.notNull(origin,"Origin is required"); Validate.notNull(destination,"Destination is required"); Validate.notNull(arrivalDeadline,"Arrival deadline is required"); Validate.isTrue(!origin.sameIdentityAs(destination),"Origin and destination can‘t be the same: " + origin); this.origin = origin; this.destination = destination; this.arrivalDeadline = (Date) arrivalDeadline.clone(); } public Location origin() { return origin; } public Location destination() { return destination; } public Date arrivalDeadline() { return new Date(arrivalDeadline.getTime()); } @Override public boolean isSatisfiedBy(final Itinerary itinerary) { return itinerary != null && origin().sameIdentityAs(itinerary.initialDepartureLocation()) && destination().sameIdentityAs(itinerary.finalArrivalLocation()) && arrivalDeadline().after(itinerary.finalArrivalDate()); } @Override public boolean sameValueAs(final RouteSpecification other) { return other != null && new EqualsBuilder(). append(this.origin,other.origin). append(this.destination,other.destination). append(this.arrivalDeadline,other.arrivalDeadline). isEquals(); } @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final RouteSpecification that = (RouteSpecification) o; return sameValueAs(that); } @Override public int hashCode() { return new HashCodeBuilder(). append(this.origin). append(this.destination). append(this.arrivalDeadline). toHashCode(); } } ? 值对象(Value Object)所包含的属性应该行程一个概念整体。当我们只关心一个模型元素的属性时,应该把它归类为Value Object。我们应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。Value Object应该是不可变的。不要为它分配粉盒标识,而且不要把它设计成像Entity那么复杂。 2.3 跨领域模型的业务规则的实现 我们使用领域服务来封装不属于领域模型或者领域模型公共的业务规则。领域服务的方法一般是静态的,并且不会更改内部状态。在装修设计预约平台里面,我们使用状态机工作流服务实现订单状态流转,它可以在设计师订单和退款单中共用。在《领域驱动设计》里面有一个示例,展示了转账服务的实现,转账动作实现的是从一个账户到另一个账户的资金流转,因此将转账设计到领域服务TransferService里面。关于服务的描述是:当领域中的某个重要的过程或转换操作不属于实体或值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为Service。定义接口时要使用模型语言,并确保操作名称是领域模型的术语。此外,应该将Service定义为无状态的。 以下是服务示例。 public class DesignerOrderWorkflowService { private DesignerOrderWorkflowService() { } private static Map<DesignerOrderState,DesignerOrderState[]> states = new HashMap<>(); static { states.put(DesignerOrderState.NEW,new DesignerOrderState[]{ DesignerOrderState.MEASURED,DesignerOrderState.ABORTED }); states.put(DesignerOrderState.MEASURED,new DesignerOrderState[]{ DesignerOrderState.QUOTED,DesignerOrderState.ABORTED }); states.put(DesignerOrderState.QUOTED,new DesignerOrderState[]{ DesignerOrderState.ACCEPT_QUOTE,DesignerOrderState.REJECT_QUOTE,DesignerOrderState.ABORTED }); states.put(DesignerOrderState.REJECT_QUOTE,DesignerOrderState.ABORTED }); states.put(DesignerOrderState.ACCEPT_QUOTE,new DesignerOrderState[]{ DesignerOrderState.PAID,DesignerOrderState.ABORTED }); states.put(DesignerOrderState.PAID,new DesignerOrderState[]{ DesignerOrderState.REFUND,DesignerOrderState.COMPLETION }); states.put(DesignerOrderState.COMPLETION,new DesignerOrderState[]{ DesignerOrderState.FEEDBACK }); states.put(DesignerOrderState.ABORTED,new DesignerOrderState[]{ DesignerOrderState.FEEDBACK }); states.put(DesignerOrderState.REFUND,new DesignerOrderState[]{ DesignerOrderState.FEEDBACK }); states.put(DesignerOrderState.FEEDBACK,new DesignerOrderState[]{ DesignerOrderState.FEEDBACK }); // 允许多次评价 } public static boolean canChangeState(DesignerOrderState state,DesignerOrderState nextState) { Assert.notNull(state,"The state can not be null."); Assert.notNull(nextState,"The nextState can not be null."); DesignerOrderState[] nextStates = states.get(state); for (DesignerOrderState possibleNextState : nextStates) { if (possibleNextState.equals(nextState)) { return true; } } return false; } public static boolean canAbort(DesignerOrder order) { return canChangeState(order.getState(),DesignerOrderState.ABORTED); } public static DesignerOrderState changeState(long orderId,DesignerOrderState state,DesignerOrderState nextState) { if (!canChangeState(state,nextState)) { BusinessException.throwException(DomainExceptionMessage.STATE_CHANGE_ILLEGAL_CODE,DomainExceptionMessage.STATE_CHANGE_ILLEGAL,orderId,nextState); } return nextState; } public static boolean isCompleted(DesignerOrder order) { return order.getState() == DesignerOrderState.ABORTED || order.getState() == DesignerOrderState.REFUND || order.getState() == DesignerOrderState.COMPLETION || order.getState() == DesignerOrderState.FEEDBACK; } } ? 3 领域模型生命周期管理 领域模型的创建会包含业务规则,我们应该将这些业务规则封装起来,使创建过程对应用层透明,这里引入Factory来实现创建。此外,对于实体,发生一系列事件后,其内部状态发生了变更,这些状态变更需要持久化,以使得应用程序能够恢复实体状态。对于值对象,我们可能也需要持久化相应的属性。这里,我们引入Repository来实现持久化管理。对于一些关联很紧密的对象,比如采购订单和商品,他们需要共同的满足一个规则(比如采购订单里面的商品的总额不能超过采购订单的限额),如果多个用户同时变更采购订单或者其包含的商品,就需要引入很复杂的锁。为了使关联紧密的对象在整个生命周期都保持一致性,我们引入了聚合Aggregate,通过它来实现一致性。 3.1 紧密关联的领域对象的一致性维护—Aggregate 首先,我们先看一下为什么要引入Aggregate。这里以采购订单为例子,采购员创建采购订单时需要指定限额,然后增加采购项目,因此可能存在两个采购员对同一个创建的采购订单进行操作,来更改订单。 如下所示,对于采购订单0012946,当前的商品金额为700,限额为1000。采购员A可能更改商品项1的数量为5,其总额为900,满足限额;采购员B可能更改商品项2的数量为3,其总额也为900,满足限额。 当采购员A、B同时提交更新后,采购订单的总额为1100,超过了1000元限额,破坏了业务规则。 在传统的方法,当我们采用以下方式更新采购订单商品,就会出现刚才破坏业务规则的情况发生。 PurchaSEOrder purchaSEOrder = purchaSEOrderBiz.getByKey(“0012946”); List<PurchaSEOrderItem> purchaSEOrderItems = purchaSEOrderItemBiz.getByOrderId(“0012946”); changePurchaSEOrderItems(purchaSEOrderItems); if (new PurchaSEOrderApprovedLimitSpecify(purchaSEOrderItems,purchaSEOrder).isSatisfied()) { purchaSEOrderItemBiz.updateBatch(purchaSEOrderItems); } ? 为了避免发生采购订单限额的业务规则被破坏,对采购订单项的变更,需要对采购订单加排它锁。 在DDD里面,引入了聚合(Aggregate)来解决这个问题。Aggregate时一组相关对象的集合,作为数据修改的单元,在整个生命周期中满足固定的业务规则。每个Aggregate都有一个根(root)和一个边界(boundary)。边界定义了Aggregate的内部都有什么,根则是Aggregate中所包含的一个特定Entity。在Aggregate中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象则可以互相引用。基于聚合,我们来实现一致的采购订单业务规则如下。 (1)应用层通过以下方式来更新聚合根里面的内容,这里必须满足一致性规则:对聚合内部实体的状态变更,只能通过聚合根来实现,通过聚合根来维持业务一致性。 PurchaSEOrder order = purchaSEOrderRepository.load(id); order.addItem(…)/removeItem(…)/updateItem(…); // 注意:这里是重点,对聚合根内部的变更,只能通过聚合根,不能通过获取内部对象进行操作 purchaSEOrderRepository.save(order); ? (2)聚合根对内部实体的状态变更如下。 public class PurchaSEOrder { private PurchaSEOrderItemRepository orderItemRepository; private List<PurchaSEOrderItem> orderItems; // …… public void addItem(int itemId,int count) { PurchaSEOrderItem orderItem = PurchaSEOrderItemFactory.create(this,itemId,count); orderItems.add(orderItem); if (!new PurchaSEOrderApprovedLimitSpecification(this).isSatisfied()) { BusinessException.throwException(…); return; } orderItemRepository.save(orderItem); this.updateTimestamp(); } // …… } ? 聚合根定义的规则如下: 3.2 领域模型的创建—Factory 当创建一个对象或创建整个Aggregate时,如果创建工作很负责,或者暴露了过多的内部结构,则可以使用Factory进行封装。领域模型的创建也可能隐含了业务规则,Factory可以向应用层屏蔽业务规则。以下是一个设计师订单的Factory类。 public class DesignerOrderFactory { private DesignerOrderFactory() {} public static DesignerOrder createOrder(int customerId,int designerId) { DesignerOrder designerOrder = new DesignerOrder(); designerOrder.setCustomerId(customerId); designerOrder.setDesignerId(designerId); designerOrder.setState(DesignerOrderState.NEW); return designerOrder; } } ? 结论:应该将创建复杂对象的实例和聚合的职责转移给一个单独的对象,这个对象本身在领域模型中可能没有职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口应该不需要上层引用要被实例化的对象的具体类。在创建Aggregate时,要把它作为一个整体,并确保它满足固定规则。 3.3 领域模型的持久化—Repository Repository的目的是实现领域对象的持久化,用于领域对象关联查询、重建、添加和删除。我们只为那些确实需要直接访问的Aggregate提供Repository,将所有对象的存储和访问操作交给Repository。如下是一个实例。 @Repository public class DesignerOrderRepositoryImpl implements DesignerOrderRepository { private static final String DESIGNER_ORDER_TABLE = "designer_order"; @Autowired private DesignerOrderMapper designerOrderMapper; @Override public void create(DesignerOrder order) { if (designerOrderMapper.create(order) == 0) { TableException.throwTableException(DESIGNER_ORDER_TABLE,TableOperation.CREATE); } } @Override public DesignerOrder selectByKey(int id) { DesignerOrder order = designerOrderMapper.selectByKey(id); buildConnection(order); return order; } @Override public DesignerOrder selectOneBySpecification(DesignerOrder example) { DesignerOrder designerOrder = designerOrderMapper.selectOneBySpecification(example); buildConnection(designerOrder); return designerOrder; } @Override public List<DesignerOrder> selectBySpecification(DesignerOrder example) { List<DesignerOrder> designerOrders = designerOrderMapper.selectBySpecification(example); buildConnection(designerOrders); return designerOrders; } @Override public void update(DesignerOrder order) { if (designerOrderMapper.update(order) == 0) { TableException.throwTableException(DESIGNER_ORDER_TABLE,TableOperation.UPDATE); } } } ? 4 结论 领域驱动设计的模式如下所示。 ? 综上,领域层的实现由聚合构成,每一个聚合通常包含了聚合根和领域模型实现、Service、工厂、Repository、领域异常等。 最终装修设计预约平台的领域模型如下所示。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |