基于DDD的程序设计,就是将前面的领域模型映射成数据架构中的程序设计,从而通过领域驱动提升软件设计质量,那么应该进行映射,让领域模型指导程序设计。
要将领域模型映射成程序设计,最终都要落实到三种类型的对象设计:服务、实体、值对象。
领域服务
有些领域的动作,他们是一些动词,看上去却不属于任何对象,它们代表了领域中的一个重要的行为,所以不能忽略他们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,最佳实践是把它声明成一个服务。这样的对象不再拥有内置的状态。他的作用仅仅是为领域提供相应的能力。Service往往是以一个活动命名,而不是Entity来命名。
例如在转账的例子中,转账(transfer)这个行为是一个非常重要的领域概念,但是它是发生在两个账户之间的,归属于实体并不合适,因为一个账户实体没有必要去关联他需要转账的账户实体,这种情况下,使用领域服务(MoneyTransferDomainService)就比较合适了。
识别领域服务,主要看它是否满足以下三个特征:
- 服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。
- 被执行的操作涉及到领域中的其他的对象;
- 操作是无状态的;
实体
我们的软件系统。毫不夸张地说,就是对现实世界的真实模拟。现实世界的事物,在软件世界中就就被模拟成一个对象。该事物在现实世界中赋予什么职责,在软件世界中就被赋予什么职责;在现实世界中拥有什么特效,在软件世界中就拥有什么属性;在现实世界中拥有什么行为,在软件世界中就拥有什么函数;在现实世界中与哪些事物存在怎样的关系,在软件世界中就应当与它们发生怎样的关联。这正是面向对象编程的核心思想。也就是DDD中,寻找领域实体的核心思想。
实体:通过一个唯一标识字段来区分真实世界中的每一个个体的领域对象。当一个对象由其标识(而不是属性)区分时,这个对象称为实体(Entity)
- 例如:公安系统的身份信息录入,对于人的身份信息可以认为是实体,因为每个人是独一无二,且具有唯一标识,且随着时间的推移人的年龄,身高,外貌会进行变化,但是标识不变。
在实践上建议将属性的校验放到实体中。
值对象
值对象:代表的是真实世界中哪些一成不变、本质性的事物,这样的领域对象叫做“值对象”。当一个对象用于对事物进行描述而没有唯一标识时,它被称为值对象。
- 例如:颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求,这避免了我们对标识追踪带来的系统复杂性。
在实践中,需要保证值对象创建后就不能被修改,即不允许外部载修改其属性。在不同上下文集成时,会出现模型概念的公用,如商品模型会存在于电商的各个上下文中。订单上下问中如果只关注下单时商品信息的快照,那么就商品对象视为值对象是很好的选择。
可变性是实体的特点,不变性是值对象的本质。
聚合根
聚合根:是一组相关对象的集合,作为一个整体被外界访问,聚合根就是这个聚合的根节点。聚合描述的整体与部分的关系:当整体不存在时,部分就变得没有意义。
- 例如:在DSP平台去创建要投放的广告订单时,我们可以将广告订单当做一个聚合根。广告订单中的投放目标(例如激活、付费等)与广告订单没有整体与部分关系。那么投放目标可以看做一个小聚合根对象。广告订单可以看着大聚合根对象(内部包含的投放目标作为值对象)。在创建广告订单时需要对投放目标进行校验,故创建广告订单实体时—可以先构建投放目标的实体(内部包含 账户信息作为值对象,投放订单部分信息作为值对象)去调用投放目标的领域服务。然后进行逻辑校验。
聚合根操作规范:
- 聚合根具有全局标识,最终负责检查规定规则;
- 聚合内的实体具有本地标识,这些标识在聚合内部才是唯一的;
- 外部对象不能引用除根实体之外的任何内部对象;
- 只有聚合的根实体才能直接通过数据库查询获取,其他对象必须通过遍历关联来发现;
- 聚合内部的对象可以保持对其他聚合根的引用;
- 聚合边界内的任何对象修改时,整个聚合的所有固定规则都必须满足。
以银行为例:Account账户是CustomerInfo实体(客户信息)和Address(值对象)的聚合根,Tansaction(交易)是流水(Journal)的聚合根,因为流水是因为交易才产生的,具有相同的生命周期。
如何创建好的聚合?
- 是否是整体与部分的关系,即聚合的部分是否完全依赖于整体,例如订单与订单明细的关系是聚合,但是订单与用户并不是聚合。
- 边界内的内容能否具有一致性:在事务中只修改一个聚合实例。如果你发现边界内很难接受强一致,不管出于性能或者产品需求的考虑,应该剥离出独立的聚合,采用最终一致的方式。
- 设计小聚合:大部分的聚合都可以只包含根实体,而无需包含其他实体。即使一定要包含,可以考虑将其创建为值对象。
- 通过唯一标识来引用其他聚合或实体:当存在对象之间的关联时,建议引用其唯一标识而非引用其整体对象。如果是外部上下文中的实体,引用其唯一标识或将需要的属性构造值对象。如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。