99.DDD解决了什么问题

239 阅读9分钟

DDD解决了什么问题

1.介绍

传统的软开开发方式要经过建模与代码实现两个步骤,而这两个步骤在传统的实现方式上在面对复杂业务系统的时候会有一些问题,这也是DDD出现的原因(DDD要解决的问题),这一章咱们来探讨以下传统方式会有那些问题

2.软件开发与业务逻辑的关系

什么是业务逻辑

  • 业务实体:例如银行储蓄领域中的账户、信用卡等等业务实体
  • 业务规则:例如借记卡取款数额不得超过账户余额,信用卡支付不得超过授信金额,转账时转出账户余额减少的数量等于转入账户余额增加的数量,取款、存款和转账必须留下记录,等等
  • 业务策略:例如机票预订的超订策略(卖出的票的数量稍微超过航班座位的数量,以防有些旅客临时取消登机导致座位空置)
  • 业务流程:例如,“在线订购”是一个业务流程,它包括“用户登录-选择商品-结算-下订单-付款-确认收货”这一系列流程
  • 完整性约束:例如账户的账号不得为空,借记卡余额不得为负数等等。本质上,完整性约束是业务规则的一部分

关系

软件用来实现业务逻辑

3.沟通问题

1746254881281.png

开发小伙伴在开需求梳理会的时候经常听到一些名词,比如某某表,某某字段等,领域专家(指精通业务的人,比如测试人员就是领域专家),听不懂也不关心这些,他们经常说领域内的名词,就是他们擅长的"行话",大家言语不统一,鸡同鸭讲,沟通成本太高,更恐怖的是有时候技术人员偏偏把某些概念理解偏了,结果九牛二虎之力码出来的代码,验收时根本不是别人想要的,有时候会出现技术人员和产品的深入交流

所以DDD要求大家(领域专家和技术人员)都是用一套术语,不要说某某表,某某字段,也不要把定好的术语口头上改成自己理解的术语

统一术语,要求每个人都使用这一套术语,各方面都不会理解错误,最终代码实现的时候,术语在代码中也要有体现,整个代码看起来就是用代码把术语给翻译了一遍

4.传统面向对象建模的问题

省略。。。

5.代码质量问题

介绍

这里代码质量问题不是指代码是否规范,而是代码是否如实的实现了业务,实现的好不好,不是指代码跑的有多快,而是业务是否清晰,业务术语,业务规则,业务流程在代码中是否有清晰的对应关系,如果一个新的小伙伴加入项目组,要改一个需求,能否通过已有的代码将业务梳理清楚,到这里大家可能想,可能吗?痴人说梦

传统MVC的问题(一)

业务背景

假设现在在做一个简单的数据统计系统,地推员输入客户的姓名和手机号。根据客户手机号的归属地和所属运营商,将客户群体分组,分配给响应业务销售组,由销售组跟进后续的业务

业务逻辑

23e40d33a0dbc6a3c9d8731ab06a7cd5.png

代码实现

107cc0185ccf5d1aab088d6ac917f9d9.png

问题
接口语义不明确

上面的方法使用了两个基本类型的参数,其他人很有可能因为不了解方法内部的逻辑或者说仅仅因为失误,他很有可能会颠倒参数的顺序,比如进行了如下的调用

d00ab76ca817808e20c9fc42e069a9f6.png

可扩展性差

上面的方法使用了姓名和手机号来完成注册,假设在未来系统开始支持通过用户名和身份证号注册,身份证号也是一个String,这时register方法可能就要被改造成registerByPhone和registerByIDCard两个方法,通过不同的方法名来吃不同的语义,假如之后又要支持同时通过手机号和身份证号注册呢?那么接口又得频繁的修改

3066d4463720273d9011dac9203dcfe1.png

参数校验逻辑

如果存在多个类似的方法,那么每个方法都要在开头进行校验,这里一定会存在大量的重复代码,而且一旦某种类型的参数校验逻辑需要修改,那么每个地方都还要一一修改这显然不符合开闭原则,将校验逻辑封装到工具类中也会存在问题比如:业务方法还是需要主动调用工具类来进行校验如果校验失败需要抛出异常参数校验异常和业务逻辑异常耦合了,一旦参数类型越来越多那么工具类中的校验逻辑也会膨胀后续也不好维护(校验逻辑过于内聚)

874349b43e648f6e6309e54d108cb628.png

09b9ad4520a9c3a0b97d291a7ae78cd9.png

业务域不够纯粹/胶水代码

register方法的核心职责应该是注册也就是把用户信息保存到数据库,但是在业务代码中存在着两个其他逻辑分别是:获取手机号的归属地编码,获取运营编码。把这两个逻辑放在注册业务领域中不太合适,让注册操作不够纯粹

胶水操作不应该出现在当前业务域中

77240ba1b60fd47e16be5071f31f4a29.png

传统MVC的问题(二)

业务背景

在上面的业务背景上新增三个功能:

  • 需要对手机号进行实名校验,实名信息通过调用外部服务获得(假设目前由中国电信提供该服务)
  • 根据外部服务返回的实名信息,按照一定逻辑计算出用户标签,记录在用户账号中
  • 根据用户标签为该用户开通相应等级的新客福利
业务逻辑

63c783f1d9b21e9bd6a43ebf8c452ee3.png

代码实现

76294d1e49afb6e17d52a32602b9c6bf.png

什么是外部依赖

一切不属于当前业务域内的设施和服务都是外部依赖,比如:数据库,数据库schema,RPC服务,ORM框架,中间件等,它们还有一个特征就是这些依赖是可以替换的,这些外部依赖要是变动会导致系统的很大变动,所以要尽可能减少外部依赖对于业务的影响

问题
面向数据表编程(schema变动导致代码变动)

上面的例子中强依赖了数据库schema,也就是DO类,如果数据库表的字段产生变动,它们对应的DO类就会产生相应的变动,而DO在这个方法里面到处都是,并且还被传递到了外部方法中,如果业务逻辑复杂个几倍并且DO产生了变化,那么这段代码可能会被改的面目全非,而且可能无意间破坏了原有的正常功能

对ORM框架的依赖

代码里面使用了Mybatis,使用Mapper来对数据表CURD,如果系统需要升级ORM框架,假如框架本身没有向下兼容API产生了变化,甚至说因为某些安全问题系统需要整个替换掉ORM框架,那么业务代码也要随之进行大量的改动,这是不合理且存在风险的

RPC服务

上面的代码中使用了中国电信提供的手机号实名信息查询服务,并且强依赖在业务逻辑中,假设中国电信提供的该接口入参和返参都产生了变动,或者说以后不在和电信产生合作而是去使用联通的服务,那么业务逻辑代码也要进行相应的修改

内部逻辑耦合

register方法的语义就是注册,在最初的代码中可以看到里面耦合了:参数一致性校验,计算用户标签,查询绑定的销售组信息,检查风控,存储用户信息。其中很多逻辑并不应该是注册这个逻辑关心的,换言之 假如计算用户标签的逻辑改变了,查询风控的实现方式改变了,还需要我注册逻辑进行相应改动的话,就会有很多问题

传统MVC的问题(三)

业务背景

假设微信账号的主键就是手机号,现在用户A想要注销自己的微信账号,那么系统是否需要通过A的手机号删除关联微信钱包信息,如果不需要该手机号被用户弃用,之后被运营商回收后发给B用户,用户B再使用这个手机号注册微信时会发现自己的账号中居然已经绑定了其他人的微信钱包,如果需要删除那么是否需要删除绑定在钱包中的银行卡信息,如果这张银行卡不仅绑定在了用户A的账号里还作为亲属卡绑定在了A的儿子小王的账户里,那么删除这张银行卡的账户信息是否会影响到小王的钱包使用?

9fcf2592c2863a3a29ee97f5157e19d3.png

问题

从上面的例子中在一些复杂的系统中对一个对象的修改可能会涉及到大量其它关联对象的状态,如何使这些对象的状态始终保持一致是个复杂的话题,从上面的例子来看对象之间的关系有点像数据结构中的图,在DDD中用聚合来描述这种关联关系

b8b84ae299c8f62ef523b42dc5805d56.png

648ef5f5a1665e65610041f604c95a27.png

聚合的价值

在复杂的软件中一个业务动作会设计大量存在关联的对象,聚合的价值就是通过封装保持所有关联对象的状态一致

在运行时领域方法通过持有聚合根维护了对象的状态一致

在持久化时Repository通过持有聚合根来处理数据一致

聚合的问题

因为聚合中存在大量对象,大量对象持久化时,我们希望每次数据更新保持最小化原则

这种问题也叫做change-tracking问题

6.模型爆炸问题

举个例子,在支付系统中存在支付与退款的业务逻辑,这两个业务逻辑都要使用支付单这个模型,在传统的开发中这两个业务逻辑会共享一个支付单模型这会导致单个模型的复杂度直线上升(因为这个模型要满足很多业务逻辑),而且当业务逻辑产生差异时,这种单模型问题会更多,在DDD中推崇模型分解的方式,不同的问题域应当使用不同的模型

04749a5813e1b14e16c23bf22da05854.png