赵玉伟的博客

领域驱动的基本元素

和实现最接近的,是领域建模后的一系列“工具”,具体来看,主要包括实体(Entity)、值对象(Value Object)、聚合(aggregate)、聚合根(Root aggregate)、领域服务(Domain Service)、工厂(Factory)、仓库(Repository)。 对于现实中所有的事物、动作等,最终会被建模成以上的几种类型,也可以称之为元素。 从现实的业务到这些模型的建模的过程,就是领域专家要做的事情,同时也是架构师必须要理解的一环,架构师如果脱离了业务场景,我认为称之为技术leader更合适。

实体

实体具有唯一性,同时具有生命周期; 两者兼备才能称得上是实体, 实体的唯一性标识在整个生命周期中不可更改。 唯一性确定了实体的不变性, 生命周期确定了实体的可变性。 比如:对人建模成person,那么person的唯一性怎么确认? 身份证ID可以确认, 如果没有身份证Id呢, 可以通过关系数据库的主键确认唯一性,如果是分布式的环境,可以借助zookeeper或者随机串产生唯一的ID,总之,ID要保持不变性; 在系统的整个生命周期中,person的状态会随时发生变化, 比如,年龄、身体状况、家庭住址、财产等等,不管这些属性怎么变,person的Id不会变,所以person是一个实体。 是否要把person建模成一个实体, 要看其在领域中的作用,比如,在汽车驾驶领域中, person只是一个试驾的人, 那么可以将其建模成一个值对象。 所以唯一性和生命周期是建模成实体的两条参考依据。

值对象

值对象比实体的约束性少,没有实体复杂, 值对象就是一个不可变的对象, 值对象具有不可变性。值对象的某个属性变了,这个值对象会变成另外一个值对象。所以一成不变是值对象的特点。比如:在订单中某个收货地址, 商品的某个规格,这些具有不变性的对象,将其建模成值对象。 我们在建模成实体的时候, 很自然的想到实体具有属性,属性可能是一个实体, 也可能是一个值对象。 而值对象中的属性可以是实体吗? 答案是肯定的, 前提是存在通过值对象找到实体的这么一条遍历路径。 具体的案例可以参考 《软件核心复杂性应对之道》中的例子。

聚合 & 聚合根

聚合是最复杂的, 或者说聚合的合理性决定了项目建模的准确性。 聚合的侧重点是整体性。 把相关的元素聚合在一起,用以组成一个业务整体。一个通俗易懂的例子:一辆车会被建模成发动机、轮子、座椅等等, 将这些聚合成一个对象(就是这些模型作为car的属性),就是一个整体, 外界不关心轮子和发动机,关心这些内部的模型是car内部的事情,所以聚合具有整体性。聚合不应该设计的太大,设计的太大会导致业务臃肿, 要合理。是否合理取决于领域专家对业务的认知。聚合要有根,外界只能操作聚合根,根内部的聚合,需要通过根来遍历或操作。比如:person是一个聚合根,其内部的属性,比如:地址、profile等应该也设计成实体或者值对象,聚合根也是一个聚合,是外界操作的发起点。充当聚合根的聚合要尽量的小, 把根内部的属性设计成其他的值对象或者实体。

领域服务

领域服务虽然也叫domain service, 但是其不同于我们在三层架构中的service。 当某些操作涉及多个实体或者聚合时, 如果把这些操作放在这些实体中,会导致实体间的依赖,破坏我们建立的领域模型,这个时候,将这些跨越领域对象的操作建模成service更加合适,service中的应该只是针对于动作建模。那么领域服务和其他的service有什么区别, 我认为主要的区别是操作对象的粒度,领域服务的实现需要考虑聚合根。

工厂

工厂顾名思义, 用来创建对象,可以特指某个充当工厂的类, 也可以指实体或者值对象中的某个工厂方法, 总之,如果通过用new Object()创建对象特别蹩脚, 用工厂实现对象的创建。 工厂创建出的类还应该具有整体性,要么按照我们的规则全部创建出来, 要么抛一个异常, 不能只创建一部分。

仓库

仓库是领域对象和数据库存储的桥梁,领域驱动不关注数据的存储,用文件、数据库存储都可以。而实际的项目中, 项目却严重依赖存储,当项目开始时,项目的数据存储其实已经确定了。仓库存在的意义,我认为更多的解决整体的包装, 在领域对象中, 每个对象做的事情时确定的, 即要发生一个明确的动作,而这个明确的动作涉及多个表的操作时,将这多个操作放到repository中,对外包装成一个有明确业务含义的接口,做到职责清晰。所以,实现上往往是在领域层中定义一个repository的接口,描述了这个接口要做的动作, 在基础设施层中实现这个接口, 在具体的实现中,调用具体的数据库框架。 领取驱动强调概念的正确性, 概念的正确性体现在某个类名、某个方法具有明确的含义, 对于存储来说, store和create具有不同的语义。 比如:我们可以在领域层定义一个接口,用来存储某个person

1
2
3
public interface PersonEventRepository{
public void store(Person person);
}

然后在基础设施层中,做具体的实现:

1
2
3
4
5
6
7
8
9
public class MybatisPersonEventRepository implements PersonEventRepository{
public void store(Person person){
// 存储基本信息
person.create(person);
// 存储地址
address.create(person.getAddress());
}
}

repository 除了负责存之外, 还负责重建对象, 也就是从数据库中获取我们一个完整的聚合,领域层不关注重建的过程,只关注重建完成后具体的业务操作。repository和factory的一个区别是,repository是从存储介质中重建, factory是从无到有的新建。

以上,是领域驱动设计用到的基本元素, 现实中所有的动、静的事物,最终将被建模成以上几种类别,同时,建立其中的关联关系。关联关系中有 1对1, 1对多, 多对多的关联。 尽量不要做双向的多对多关联, 按照业务重点,做成单向关联。 结果是明确的,过程是模糊的, 重点在于业务的理解和建模。要在某个领域内有足够深入的积累是建模的前提。