没错,又来了,一个项目的结束,就会复盘并完善下。
传统开发的弊病:
-
通过事务脚本模式来开发需求;
-
开发人员热衷于技术并通过技术手段解决问题,而不是深入思考和设计业务的走向;
-
过于重视数据库,围绕数据库和数据模型进行建模,按数据流程进行建模;
-
按技术视角进行业务命名,导致后续迭代以及人员更替时,产品和技术无法对齐;
-
随着业务的发展,到后续业务、技术无法沟通,各种不理解;
-
业务希望技术出排期,技术得撸代码,耗费精力;
-
代码开发的过程中技术和业务耦合,一个场景一个服务,代码流水线;
-
因为技术的问题会导致业务流程的中断,导致异常问题过多;
-
过度方案太多,没有定期消除,导致后续的代码越来越难维护;
-
业务只加需求,不减过期的需求,导致软件越来越臃肿;
DDD解决了什么?
-
通用语言,让开发和业务在语言上统一;
-
战略设计(业务和开发都能看到的软件模型)
-
边界划分(通过限界上下文来划分,在不同的情景下域的作用是不同)
-
领域划分(通过域来确定业务领域,包含核心域、子域、支撑域)
-
分层架构(整洁架构)
-
战术设计
-
聚合(值对象,实体,聚合根)
-
领域事件
最终要达到的目的:
-
业务和技术能够通过领域模型建立通用语言(业务和技术的沟通无障碍);
-
业务边界清晰(微服务的拆分);
-
业务模型独立且模型具备可测试性;
-
技术实现独立,可以随着业务的发展不断的更迭;
名词解析
事务脚本-面向过程
-
事务:执行的业务
-
脚本 一组系统执行的操作,和用户的逻辑关联;
根据接口请求,将业务逻辑通过面向过程组织为解决方案的过程;特点:
-
简单 (仅依赖面向对象语言的少量功能)
-
快速 开发快、上线快
-
扩展性查
-
改动以后测试性差
-
业务复杂后,代码容易变成“大泥球”,系统腐化速度和复杂性呈指数级上升
领域模型
通过面向对象的设计,将业务实现模型化(将类的状态和行为分离);
特点:
-
还原现实世界(复杂)
-
责任清晰
-
边界清晰
-
完全面向对象,状态和行为分离;
-
扩展性强
-
测试性强
-
简单的业务用领域模型反而复杂了;
如何理解领域?
-
领域逻辑是显性的专业知识,符合事实逻辑(生活中的事务,必须满足一致性),可以很容易的推导出来;
-
领域逻辑是提纯、通用的规则,不管我们的业务流程怎么变,我们的领域逻辑是不变的,所以领域一定是正确的,一定是合规的(合理性不由领域控制);
-
领域逻辑是稳定的(不易变,易变的规则都应该放到应用层)
就像我们做的会员系统,用户在购买会员的时候
-
参数校验 application层
-
防重校验 application层
-
有支付中的会员订单不能购买(随着业务的变动,易变)application层
-
会员不能重复购买(随着业务的变动,易变)application层
-
库存扣减与初始化订单 (不变)领域层
-
支付(支付领域)领域层
-
会员初始化 (不变)领域层
DDD
是对面向对象设计的改进,开发复杂业务逻辑的一种方式,但不是银弹。
特点:
-
通过领域划分有助于把应用程序分解为服务;
-
每个服务都有自己的领域模型;
-
限界上下文清晰
实体
具有持久化ID的对象,能唯一标识一个条记录的;
特点:
-
标识(identity)唯一不可变,且能区分出具体的事物
-
连续性(continuity),贯穿整个生命周期
-
只有根实体对外暴露(但不排除一些为了冗余而暴露别的信息)
在我们的系统里,用户实体的标识:包含id和租户,只有这两个组合我们才能唯一识别出这个用户
public class CustomerId {
private Long id;
private Integer tenantId;
public CustomerId(Long id, Integer tenantId) {
this.id = id;
this.tenantId = tenantId;
}
public boolean isRealName(){
if (Objects.isNull(this.id) || this.id == 0L){
return false;
}
return true;
}
}
值对象
⽤于描述状态的属性,特征,只关心对象是什么,不关注唯一性。
脱离了主体对象就没有任何意义,比如会员的有效期;
public class VipPeriod {
/**
* 当期会员开始时间
*/
private Date currentStartTime;
/**
* 当期会员过期时间
*/
private Date currentExpiredTime;
/**
* 会员有效连续性的开始时间
*/
private Date validStartTime;
/**
* 最终过期时间
*/
private Date lastExpiredTime;
public Boolean isVip(){
//是否会员判断逻辑
}
public VipStatus vipStatus(){
//获取状态
}
//续费会员计算
public void renew(VipType vipType){
//根据不同的会员类型,做不同的计算
addMoth();
}
//续费月会员逻辑
private void addMoth(){
}
//续费季会员逻辑
private void addQuarter(){
}
//续费季会员逻辑
private void addQuarter(){
}
}
聚合根
把一组有相同生命周期、在业务上不可分离的实体和值对象放在一起;
-
定义了对象之间清晰的关系和边界,实现领域模型的内聚;
-
必须将聚合根作为一个修改数据的单元(我们是在对应的领域服务中处理)
-
一个聚合必须有一个聚合根,根是聚合中的一个实体;
-
对一个聚合中实体的访问或操作,必须通过这个聚合的聚合根开始;(我们的聚合根只有访问,和聚合根的公共方法)
-
在对聚合进行查询或操作时,整个聚合是作为一个整体;
public class VipEntity {
/**
* 主键id
*/
private VipId vipId;
/**
* 用户id
*/
private CustomerId customerId;
/**
*会员类型
*/
private VipType vipType;
/**
*会员有效期(开始时间,失效时间)
*/
private VipPeriod vipPeriod;
}
领域事件
由特定领域,因触发一个动作而触发的发生在过去的行为事件
特点:
-
动作(一个行为的发生产生的)
-
已发生
工厂
负责聚合根、实体的创建,以及各层之间数据的转化(有些可能内聚到了实体中,但不能污染领域模型);如:
-
adapter 到领域层的数据转化
-
基础设施层到领域的转换
建模
什么是建模?
建模分为数据建模和业务建模。业务关注的是我的流程怎么往下走;技术关注的是我该用什么样的技术手段去保证业务的完整性,以什么样的数据模型去承接业务;最终业务和技术虽然实现了功能,但业务与技术并没有在理念上对齐。
领域建模是为了解决上面的问题。
建模的本质是归类抽象。 目的:
-
减轻认知的负担,最终将业务的心智模型提取出来,显性化,业务和技术达成一致;
-
避免重复的思考与工作(内聚)
-
提升人的效率(关注更高层次的内容,而不是陷入大泥球中)
架构
DDD分层架构
DDD分层架构中有很重要的依赖原则:每层只能与位于下方的层发生耦合,类似于网络的7层或TCP/IP的4层模型架构,每一层各司其职,并且只关心向下一层的实现,而不会出现各层耦合。
洋葱架构
同心圆代表软件的不同部分,从里向外依次是领域模型,领域服务,应用服务和外层的基础设施和用户终端。洋葱架构根据依赖原则,定义了各层的依赖关系,越往里依赖程度越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的情况,
六边形架构
将应用分为内六边形和外六边形两层,内六边形实现应用的核心业务逻辑。外六边形完成外部应用,基础资源等的交互和访问,对于与不同的外部系统交互,由外六边形的适配器负责协议转换,保证内六边形业务逻辑的干净。
四种领域模型
-
失血模型:领域对象只包含setter和getter方法,业务逻辑放到service
-
贫血模型:包含属性的getter/stter和所有业务逻辑;
-
充血模型:包含属性的getter/stter和所有业务逻辑,再加上与数据库的操作。这是DDD提倡的模型
-
胀⾎模型,删除Service层,所有逻辑都放到模型中,模型直接对接web层
通过什么手段来落地?
-
事件风暴
-
用例+用户故事
我们是怎么做的?
-
越底层越稳定,越上层越易变 (高内聚,低耦合)
-
domoin层捕捉业务规则,应用层捕捉应用逻辑;
-
adapter完成验签,解密,适配各端的到应用服务的逻辑(如注册发短信,h5,安卓,ios都不太一样)
-
应用层将是应用逻辑的组装,包含了技术以及领域服务的调用,以及易变的逻辑;
-
使用半贫血模型;
-
聚合根里包getter和setter,包含部分公用的业务逻辑(主导查询)但不包含所有的业务逻辑;
-
一个聚合根可能支撑多个查询,每个查询可能只包含聚合根的部分内容
-
领域服务包含操作型的业务逻辑(主导操作)
-
domain包含领域模型、领域服务,用于捕捉领域逻辑,暴露出的服务用于操作领域模型
-
domain作为底层,依赖倒置基础设施层;
-
domain层通过gateway,做了一层防腐,gateway就是一组接口;
-
复杂业务使用领域服务构建,简单业务直接应用层调用基础设施层;
分享后的大家问的几个问题
-
事务如何添加?
-
如果整个领域服务是一个事务,则在application层进行加事务,在application里需要注意,不要破坏领域;
-
如果只是其中领域服务的一个小方法,可以在gateway的实现里实现;
-
性能问题如何解决?性能属于技术问题,和业务无关;
-
比如常见的CQRS,将写和读分离,写异步
-
先写redis再写数据库
-
外部调用短连接变长链接;
-
事实数据做宽表;
-
事件驱动
-
采用哪种架构(六边形、洋葱架构)
-
最核心的整洁架构+DDD的分层架构,并没有特别严格;
-
最核心的是业务逻辑和与模型;
-
随着业务的发展与技术发展不断的适配;
-
context的作用以及放的位置?
-
context应该只作用域application层
-
context对应的是当前application的上下文
-
在application里会根据不同的领域模型拆解到对应的聚合根上,或领域实体上;
-
一个context可能对应多个接口,但是一定对应一一种场景
-
聚合根、实体、值对象的关系(拆解每个人一个理解,还没想好怎么标准化操作)
-
聚合根是根据业务走,用例图能很好的表达,我们操作的领域
-
值对象,一组字段组合起来能标识出一块内容的,比如会有的有效期,几个字段组合起来就能体现会员是否有效,有效期脱离了会员又没有意义;
-
聚合根不能引用聚合根
-
实体或值对象能出现在多个聚合根里
参考:
https://blog.csdn.net/significantfrank/article/details/110934799
https://www.cnblogs.com/jiyukai/p/14830869.html
https://zhuanlan.zhihu.com/p/115685384
https://www.zhihu.com/question/25089273