实践 DDD 领域驱动设计

说明

领域驱动设计最近又火了。概念不断被提及,但是相信对于像笔者一样的很多开发者对于其如何应用都一头雾水。

正如《实现领域驱动设计》中作者提到的不同公司的业务能力开发能力和成熟度不一样, DDD为了解决复杂业务为生,并不适合所有的软件项目, 对于很多初创公司而言,业务本身就是模糊的,只是需要做出一个MVP(最小可行性产品)来试探商业模式 ,采用ddd显得过“重”了一点,反而给团队成员带来额外的负担,所以 团队 管理者首先应该关注的是软件系统是否值得做出DDD投入

不过不管黑猫白猫,能抓到老鼠就是好猫。 我们以电商业务来演练和实践ddd的部分理论,并解释ddd的概念。

战略设计阶段

领域:即业务是属于哪块,电商领域,保险领域,零售领域,又可细化分为子领域。如电商下(订单交易领域、库存领域、会员领域、物流领域….)

领域专家:一般指熟悉对应领域的产品经理项目经理  

子域:子域可细分为核心子域、通用子域和支撑子域,简单理解为哪部分是比较核心的就可称作核心子域,哪些功能偏边缘化叫支撑域。

互联网公司的敏捷开发模式的产品研发流程从收集需求、PRD评审、技术模块拆分这些前期流程。而战略设计即在这个阶段完成,顾名思义侧重于从宏观上对业务进行拆分,和对未来走向的预测。

DDD的目的是为了领域专家更好地与开发进行沟通合作,使得代码更好地传达业务规则。

最初的需求方可能会提一些凌乱的需求。

为了更好地传达规则, 领域专家将需求整合提取出领域的概念, 技术/项目管理者一起划分好子域和限界上下文。各方统一所谓的 通用语言 并在以后的协作中使用 。比如电商业务中双方约定好中商家用户和买家用户的概念,用户和账户的概念,交易订单和支付订单、物流订单、售后订单的概念,以实现后期沟通顺畅。

最终产出:划分出了哪些子领域、上下文映射图是怎样的。

贴一个电商业务的上下文映射图(下单交易上下文为下游,其他皆为上游):

而各个上下文的交互方式,即 集成限界上下文 的方式其实就是我们常说的系统交互方式:RPC调用、REST接口调用、消息队列通信。

怎么理解限界上下文和子域的关系?

限界上下文:是一个显示边界,领域模型即存在于这个边界之内。在边界内,通用语言有特定明确的意义。

ps:是不是觉得每个字都认得,但是不知道表达了什么意思….

在理解限界上下文和子域上确实有点费劲。笔者当时的疑惑主要是:为什么要用上下文映射图而不是子域交互图,子域划分出来不就说明边界已经明确了么,为什么还专门搞一个限界上下文的概念。

关于限界上下文,贴一下个人理解:

接下来要咬文嚼字一些了。

1、从“限界”二字来说。限界上下文明确了业务范围和职责边界。针对上面问题“子域中不是已经有边界的概念了么”。可以思考一个有意思的事,子域有边界,还是说因为有了边界才有子域。听到过一个非常到位的类比: 如果没有细胞壁,如何定义细胞质 ?

2、从“上下文”来说。上下文关注的是两个系统交互时的环境,或者说语境。

举个例子:小学是一个子域,中学是一个子域。升学这个事件动作则要上下文表达。

通用语言要在限界上下文(语境)中保证其明确意义。举个例子,商家管理上下文中,我们(平台)说的用户指的是商家而不是买家,支付上下文中,我们以支付单为核心,语境无需引入物流单、库存等词汇。

通常来说,我们可以近似地认为子域和限界上下文一一对应的。

战术设计阶段

截止到此,我们已经划分好了子域,对开发人员来说,已经拆分好了项目。各个团队可以针对自己的子域进行独立开发了。对于ddd而言,我们开始进行战术设计阶段。战略设计关心做什么,战术设计则更关心技术实现细节,即:怎么做。

先说下ddd中推荐的 六边形架构

这里要吐槽一下六边形架构这个命名,搞得好像有六个什么东西一样。其实只是根据视觉形状起的名。现在叫端口与适配器架构。

转换一下是这样的

严格分层架构:某层只能与直接位于其下方的层发生耦合

松散分层架构:允许上方层可以与任意下方层发生耦合。

这里采用松散分层架构。也可按依赖倒置原则, 基础层依赖领域层的一些东西(常见的就是实体Entity)

适配器层:

负责接口转换,即是最外层请求处理类,将外部请求转化为内部API能理解的输入。对于REST接口可能是一个controller类;

对于dubbo调用来说是开放出去的provider服务类;

对于grpc调用来说,是protobuf请求对象转换处理类;

对于消息机制来说,对应的是消息的监听器

如此采用端口适配器模式,可以不影响内部服务,只需在适配器层进行增改。尽管大家不知道六边形架构的定义,但相信很多人是这样做的。无须赘述。

应用层:负责协调领域层的接口实现前端展示或返回需要。

领域层:定义领域实体和逻辑。包括实体、值对象、领域服务、领域事件、资源库。

基础层:如数据库相关。

实体和值对象

实体:

有唯一业务标识


有自己的业务属

性和行为


属性可变,有自己的生命周期

值对象:  可以有唯一业务标识

有自己的业务属性和行为

一旦定义不可改变

二者的关系可总结为: 值对象关心对象是什么样的,实体侧重描述对象是哪个?

提到有自己的业务属性和行为这块,想想这不就是我们年轻时说的面向对象编程的思想码?

但是回顾一下会发现,这个思想好像被很多人抛诸脑后很久了,定义的对象类都成了一个个的pojo,只有属性和属性对应getter和seter方法,也就是ddd中所说的 失血模型。

失血模型:只含属性和对应的getter/setter方法,无业务处理逻辑

贫血模型:包含不依赖持久化的部分领域逻辑,依赖持久的逻辑被放在领域服务层。

充血模型:绝大数业务逻辑都放在其中,包括持久化逻辑。少数不适合的逻辑被提取出来放在领域服务层中。

胀血模型:主张不需要领域服务层,把一些业务逻辑都放在模型对象中处理

领域服务

胀血模型主张把所有的业务逻辑放在模型中处理,但是事实上有些逻辑并不是适合放在某个领域对象中处理。比如

1、领域对象间的转换

2、某些场景下需要多个领域对象作为输入值,结果产生一个值对象。

区别于应用层的服务,领域服务处理的是业务逻辑。应用层负责对领域服务处理结果进行渲染和组装返回给前端。ps: 针对查询类的操作,建议作为应用层的查询服务单独拎出来,因为查询尝尝涉及到多个维度的查询,或把多个领域对象的查询结果组装成一个返回值对象。

举个栗子:订单领域需要向商家(PC端)和买家(APP端),商家端按发货条件时间查询所有买家的订单,操作发货处理售后等流程。买家端完成下单、查询个人订单。在应用层我们抽象三个应用处理出来,公用的查询应用、商家端应用(关联商家权限控制上下文)、买家端应用(关联买家权限控制上下文)。

领域事件

领域事件即领域业务周期中一些关键行为,关键的定义是其他地方需要依赖此事件推送业务流转。如订单被支付这个事件,需要触发库存扣减、商家待结算账户余额增加等操作。关于领域事件处理方法:跨子域处理常用消息队列发布/订阅模式,同项目处理常用注册事件监听器处理。不多描述。

聚合和资源库

领域对象之间常常有依赖关系。比如主订单-子订单(商品)项,子订单依附于主订单存在,二者的关系称作 聚合 ,主订单作为 聚合根。

@Data
public class TradeOrderEntity {
//订单号
String orderNo;
// 商品详情
List orderItemDetails;// ...}

我们通过为每一个聚合选择一个根,并通过根来控制所有对边界内的对象的访问。 外部对 象只能持有根的引用; 由于根控制了访问,因此我们无法绕过它去修改内部元素。所以在ddd中,资源库Repository 是面向聚 合根操作的,可对多个dao对象 的组合使用。

@Repository
public class TradeOrderRepository {

@Autowired
TradeOrderMapper tradeOrderMapper;

@Autowired
OrderItemDetailMapper orderItemDetailMapper;
}

关于其中一些思想,笔者也没有想清楚,所谓知行合一,恐怕很多方法论要真的遇到问题了才会理解其价值。先整理到这。最后针对Java后端开发同学,提供一个模块分层的框架供参考

思考

无忌,我教你的还记得多少?” “回太师傅,我只记得一大半”

“ 那,现在呢?” “已经剩下一小半了”

“那,现在呢?” “我已经把所有的全忘记了!”

“好,你可以上了…”

END