基础概念
领域
领域就是用来确定范围的,范围即边界,这也是DDD在设计中不断强调边界的原因。简言之,DDD的领域就是这个边界内要解决的业务问题域。领域可以进一步划分为子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问拆分为小问题的过程。
其实好理解,领域会细分为不同的子域他们分别是:核心域,通用域和支撑域
决定产品和公司核心竞争力的子域就是核心域,他是业务成功的主要因素和公司的核心竞争力。没有太多个性化的述求,同时被多个子域使用的通用功能子域就是通用域。还有一种功能子域是必须的,但即不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,就是支撑域
通用域例子:比如认证、权限,这些都很容易买到。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等
聚合根和领域服务负责封装实现业务逻辑。领域服务负责对聚合更进行调度和封装,同时可以对外提供各种类型的服务,对于不能直接通过聚合根的就需要通过领域服务。
聚合根其实就是本身无法完全处理这个逻辑,例如支付这个逻辑,订单聚合不可能支付,所以在订单聚合上架一层领域服务,在领域服务中实现支付逻辑,然后应用服务调用领域服务。
遵守以下规范:
1.同限界上下文内的聚合之间的领域服务可直接调用
2.两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配
例子,用户升职,上机领导要变,上级领导的下属要变代码如下
限界上下文
限界就是领域的边界,而上下文则是语义环境,通过领域的限界上下文,我们可以在统一的领域边界内用统一的语言进行交流,简单来说限界上下文可以理解为语义环境。
综合一下,我认为限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证领域之内的一些术语,业务相关对象等有一个确定的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确的知道什么应该在模型中实现,什么不应该在模型中实现。
如果没有具体的语义环境,还真不太好理解。但是,如果你已经知道了这句话的语义环境,比如是寒冬腊月或者是炎炎夏日,那理解这句话的涵义就会很容易了。
所以语言离不开它的语义环境。
而业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。
正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。
理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
贫血模型和充血模型
贫血模型
贫血模型具有一堆属性和set get方法,存在的问题就是通过pojo这个对象上看不出业务有哪些逻辑,一个pojo可能被多个模块调用,只能去上层各种各样的service来调用,这样以后当梳理这个实体有什么业务,只能一层一层去搜service,也就是贫血失忆症,不够面向对象。
充血模型
比如如下user用户有改密码,改手机号,修改登录失败次数等操作,都内聚在这个user实体中,每个实体的业务都是清晰的,就是充血模型,充血模型的内存计算会多一些,内聚核心业务逻辑处理。
说白了就是,不只是有贫血模型中setter getter方法,还有其他的一些业务方法,这才是面向对象的本质,通过user实体就能看出有哪些业务存在。
@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {/*** 用户名*/private String userName;/*** 姓名*/private String realName;/*** 手机号*/private String phone;/*** 密码*/private String password;/*** 锁定结束时间*/private Date lockEndTime;/*** 登录失败次数*/private Integer failNumber;/*** 用户角色*/private List<Role> roles;/*** 部门*/private Department department;/*** 用户状态*/private UserStatus userStatus;/*** 用户地址*/private Address address;public User(String userName, String phone, String password) {saveUserName(userName);savePhone(phone);savePassword(password);}/*** 保存用户名* @param userName*/private void saveUserName(String userName) {if (StringUtils.isBlank(userName)){Assert.throwException("用户名不能为空!");}this.userName = userName;}/*** 保存电话* @param phone*/private void savePhone(String phone) {if (StringUtils.isBlank(phone)){Assert.throwException("电话不能为空!");}this.phone = phone;}/*** 保存密码* @param password*/private void savePassword(String password) {if (StringUtils.isBlank(password)){Assert.throwException("密码不能为空!");}this.password = password;}/*** 保存用户地址* @param province* @param city* @param region*/public void saveAddress(String province,String city,String region){this.address = new Address(province,city,region);}/*** 保存用户角色* @param roleList*/public void saveRole(List<Role> roleList) {if (CollectionUtils.isEmpty(roles)){Assert.throwException("角色不能为空!");}this.roles = roleList;}
}
实体和值对象
实体
实体和值对象这两个概念都是领域模型中的领域对象。实体和值对象是组成领域模型的基础单元。
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在DDD里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都住在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品
在领域模型映射到数据模型时,一个实体可能对应0个,1个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一 。在某些场景中,有些实体只是暂驻在静态内存的一个运行态实体,他不要持久化,比如基于多个价格配置数据计算后生成的折扣实体。
而在有些复杂场景下,实体与持久化对象则可能是一对一或者多对一的关系,比如吗,用户user与角色role两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息customer和账户信息account两类数据保存到同一张数据库表中客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景,
@NoArgsConstructor
@Getter
public class User extends Aggregate<Long, User> {/*** 用户id-聚合根唯一标识*/private UserId userId;/*** 用户名*/private String userName;/*** 姓名*/private String realName;/*** 手机号*/private String phone;/*** 密码*/private String password;/*** 锁定结束时间*/private Date lockEndTime;/*** 登录失败次数*/private Integer failNumber;/*** 用户角色*/private List<Role> roles;/*** 部门*/private Department department;/*** 领导*/private User leader;/*** 下属*/private List<User> subordinationList = new ArrayList<>();/*** 用户状态*/private UserStatus userStatus;/*** 用户地址*/private Address address;public User(String userName, String phone, String password) {saveUserName(userName);savePhone(phone);savePassword(password);}/*** 保存用户名* @param userName*/private void saveUserName(String userName) {if (StringUtils.isBlank(userName)){Assert.throwException("用户名不能为空!");}this.userName = userName;}/*** 保存电话* @param phone*/private void savePhone(String phone) {if (StringUtils.isBlank(phone)){Assert.throwException("电话不能为空!");}this.phone = phone;}/*** 保存密码* @param password*/private void savePassword(String password) {if (StringUtils.isBlank(password)){Assert.throwException("密码不能为空!");}this.password = password;}/*** 保存用户地址* @param province* @param city* @param region*/public void saveAddress(String province,String city,String region){this.address = new Address(province,city,region);}/*** 保存用户角色* @param roleList*/public void saveRole(List<Role> roleList) {if (CollectionUtils.isEmpty(roles)){Assert.throwException("角色不能为空!");}this.roles = roleList;}/*** 保存领导* @param leader*/public void saveLeader(User leader) {if (Objects.isNull(leader)){Assert.throwException("leader不能为空!");}this.leader = leader;}/*** 增加下属* @param user*/public void increaseSubordination(User user) {if (null == user){Assert.throwException("leader不能为空!");}this.subordinationList.add(user);}
}
值对象
简单来说,值对象本质上就是一个集。那么这个集合里面有什么呢?若干个用于描述目的,具有整体概念和不可修改的属性。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎
/*** 地址数据*/
@Getter
public class Address extends ValueObject {/*** 省*/private String province;/*** 市*/private String city;/*** 区*/private String region;public Address(String province, String city, String region) {if (StringUtils.isBlank(province)){Assert.throwException("province不能为空!");}if (StringUtils.isBlank(city)){Assert.throwException("city不能为空!");}if (StringUtils.isBlank(region)){Assert.throwException("region不能为空!");}this.province = province;this.city = city;this.region = region;}
}
值对象与实体一起构成聚合。值对象逻辑上是实体属性的一部分,用于描述实体的特征,值对象创建后就不允许修改了。只能用另一个值对象来整体替换。值对象是一些不会修改,只能完整替换的属性值的集合,你更关注他的属性你更关注他的属性和值,它没有太多的业务行为,用于描述实体的一些属性集,被实体引用,依附于实体的值对象基本没有自己的数据库表。是否要设计成值对象,你要看这个对象是否后续还会来回修改,会不会有生命周期。如果不可修改,并且以后也不会专门针对它进行查询或者统计,你就可以把它设计成值对象,如果不行,那就设计成实体吧。
在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
能会设计出不同的结果。有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。
聚合
实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为,而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象
你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。
聚合根
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
第一步:采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单,标的,客户,被保人等
第二步:从众多实体中年选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一ID,是否可以创建或修改其他对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体
第三部:根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出一个包含聚合根(唯一)多个实体和值对象的对象聚合这个集合就是聚合。在图中我们构建客户和投保这两个聚合
第四步:在聚合内根据聚合根,实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我们需要说明一下:投保人和被保人的数据,是通过关联客户ID从客户聚合中获取的。在投保聚合里他们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变根,也不会影响投保单的值对象数据,从图中哦们可以实体之间的引用干洗,比如在投保聚合里投保单聚合根引用了报检单实体,报价单实体则引用了报价规则子实体
第五步:多个聚合根据业务语义和上下文一起划分到同一个限界上下文内
这就是一个聚合诞生的完整过程了
领域事件
领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作,也可能是定时批处理过程中发生的时间,比如批处理生成季缴费通知单,触发发送缴费邮件通知操作,或者一个时间发生后处触发的后续动作,比如密码连续输错三次,触发锁定账户的动作
在做用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件