复杂软件解决之道[6]小试牛刀,教练我想写代码
共 4134字,需浏览 9分钟
·
2021-04-18 19:52
复杂软件开发之道系列进行到现在,主要讲的还是理论和思考,但是如何针对DDD编写代码这一问题想必是大家一直关心的问题。
本文我们就小试牛刀,展示一下通过DDD方式编写代码。
1.只有新项⽬才能考虑⽤DDD吗?
「当然不是」,DDD这套⽅法论不仅适合从零开始的新项⽬的建模,⽽且适 合复杂业务系统的重构。当然如果能在新模型的建⽴的过程中就使⽤DDD作为指导,是最好不过的事情,原因在于你会省略对原 有业务系统代码逻辑的梳理和适配过程,众所周知,这不是⼀件容易的事情。
2 我想在遗留项⽬重构中使⽤DDD,我该怎么做?
emmmm,恭喜你提出了⼀个好问题,这个问题展开讲的时⻓甚⾄会超过我们本次分享的所有内容加起来的时⻓。(展开讲涉及到领域建模的战略 战术设计,这个后续专题分享) 不过这个问题的本质还是值得探究⼀番。
⾸先,遗留系统,DDD这两个加起来本就不是那么容易的事情,所以⼤家难免有以下顾虑:
- 懂这块儿业务的同学并不多;
- 没有足够的落地资料进行参考;
- 对使用DDD没有底气,不知道从何下手;
- 客观上,开发周期紧张,排期难以准确预估;
- 代码逻辑复杂,梳理起来具备一定难度;
- 对原有模型的二次抽象过于复杂,不知从何下手。
有顾虑在所难免,但是如果我们⽤了DDD去建模并且最终成功的落地了这件事情,对我们有什么收益或者说好处呢?好处/收益有如下几点:
- 最直接的受益是对DDD具备了落地经验;
- 对复杂业务重构具备了落地经验;
- 积累了丰富的文档和资料,方便新人快速理解业务并上手;
- 能够对领域划分有所思考和实践;
- 使业务代码腐化速率降低,稳定支持一定时间内的正常迭代;
- 客观上拥有了一个具备稳定业务核心的系统,方便快速迭代核心外的业务逻辑,而且可以尽量保证内部模型健壮沉稳地发展;
- 对于参与者本身的沟通能力和思考能力也是十分明显的。
伟⼈说,道路是曲折的,前途是光明的。对于DDD⽽⾔亦如是,只要最终得以落地,那么我们的受益就会⼤幅度 ⾼于产出,对于我们RD⾃身的影响也是⻓⾜可观的。那么具体应该如何去做呢,我们需要按照图中的这些步骤去做:
3. 具体的代码是如何应⽤DDD建模思想的
限于篇幅和准备时间,我们通过⼀个简单的业务场景,展示⼀下充⾎模型、领域事件发布、 命令查询分离的代码书写⽅式,权当抛砖引⽟了。
❝背景介绍,我们就⼀个简单的为账户进⾏加款这个业务场景,通过DDD的分包和编码⽅式,直观展示⼀下具体的 代码编写⽅式。这个场景的主要流程如下图
❞
这个过程中涉及到的对象及其关系⼜是怎样的?
通过名词动词法,我们能够得出以下的交互过程
- [执行]->转账请求->[发布]->转账请求已接受事件
- 账户->[执⾏]->充值动作->[发布]->账户已完成事件
引⼊命令查询模式,旨在展示另⼀种编码⻛格,不⽤命令查询模式(CQRS)也是可以的。这⾥想补充的是,通过DDD编码没有⼀个统⼀的规范或者样板,我这⾥展示的也并不⼀定是最优的,但是我们始终还是要秉承⼀个观念「“还对象以⾏为,⽤代码映射真实世界”」。
简单看⼀下代码的结构,采⽤了经典的四层分包进⾏组织:
展开来看如下:
3.1 通过use case导向,从业务⻆度去看代码,最后再进⾏总结分析
❝⾸先是账户充值请求的⼊⼝,发⽣在application层,这⼀层主要提供⽤例级别的⽅法逻辑。
❞
通过⼯⼚对命令进⾏转换,转换为内部的请求对象,先对请求持久化之后,认为请求已接受,随即发布事件(事件⼀般都是过去式,此处为充值请求已接受事件),事件发布后,会有监听器进⾏处理。
从事件中还原请求,并转换为扣款命令,再次请求账户应⽤服务的账户增加服务。
账户实体通过id唯⼀标识,从持久设施中取出后进⾏处理,⽽不是直接传递该实体,这也是DDD编码中的⼀个实践,传递标识,⽽不是直接传递引⽤。
尽量弱化代码耦合 获取到账户实体,通过账户⾃身的⾏为执⾏充值,这⾥其实就是所谓的「充⾎模型」,即:
❝领域⾃身的职责 让领域对象⾃⼰完成,⽽不是借助事务脚本完(传统的xxxxService就是事务脚本,将⾏为从领域对象中 抽离,领域对象退化为数据承载的容器,或者说就是⼀个数据结构)
❞
通过领域对象的⾏为完成充值之后,发布充值完成事件,将变更后的账户持久化(借助Spring的同步事 件发布订阅,⽀持事务传播机制)。
账户更新事件监听器,对事件进⾏处理,将充值完成的账户持久化到持久化设施,如DB中。
通过仓储持久化变更的账户余额(可以使⽤乐观锁,此处不实现)。
小结DDD编码
简单的总结下吧:
我们通过这个简单的 「账户充值请求接收->执⾏账户充值->充值完成后更新账户」 的业务场景, 通过领域驱动的⽅式 进⾏编码,可以看到最直接的变化就是:我们把业务的核⼼实现放在账户聚合的内部,而账户实体成为⼀个充⾎模型。
借助命令查询分离(查询我没有实现,并不复杂,⼤家感兴趣可以⾃⾏了解),领域事件发布订阅等⽅式,以不同于传统的事务脚本的⽅式实现了这个业务。
我们通过这个简单的流程,能够很容易根据事件的发布划分出整个业务流程执⾏的不同阶段,通过领域⾏为可以将核⼼业务操作牢牢把握在领域内部,保证我们的模型不会更快的随着需求迭代⽽腐化。
样例确实简单了些,主要是笔者的时间确实⽐较紧张,权当抛砖引⽟,后续有机会针对编码这块⼉还有更多的分享。
补充:对DDD上下文映射的思考
软件是对现实世界的抽象和映射,如下图:
软件发展的规律就是逐步由简单软件向复杂软件转变,使变更总是满⾜当下的需求,通过领域努⼒还原 真实世界,避免过度设计,让代码具备开放封闭的特性,能够避免软件过快腐化。
对于上下文而言,上下⽂关系有以下几种:
- 「U(Upstream 上游)/D(DownStream 下游)」 : 上游的变动会影响下游,⽐如下游在代码上依赖上游(或者 模型结构),这⾥的上下游不是指数据的流向。
- 「OHS(Open Host Service 开放主机服务/发布语⾔)」 : 上游定义⼀种协议,让下游通过协议去使⽤该服 务,并公开这份协议(接⼝),让想⽤的⼈可以使⽤它。
- 「PL(Published Language 发布语⾔)」 : 协定传送资料的语⾔格式,如 XML、JSON 或是 Protocol Buffer等等。
- 「ACL(Anti-corruption Layer 防腐层)」 : 是⼀种在不同模型间转换概念与资料的机制。为了要避免你 Bounded Context 的 domain model 概念受到来⾃外部的污染,你可以藉由 ACL 来建⽴⼀层隔离层,利 ⽤Facade模式来将外部概念转换成内部 Domain Model 能理解的概念。此种关系多⽤于遗留系统和团队 外部系统。
- 「Shared Kernel(共享内核)」 : 两个 Bounded Context 共⽤同⼀个模块。当两个团队在开发同⼀个应⽤程 序,并且各⾃的 Bounded Context 共享⼀块重复的领域知识,为了加快开发的脚步,就会将部分共⽤的逻辑抽成 Shared Kernel。这也表明这部分的业务语⾔和领域知识在共享的两个上下⽂是不存在歧义,可以通⽤的。「例如」:两个 Bounded Context 依赖同⼀个⼆⽅库。
- 「Customer-Supplier(客户-供应商)」 : 当 Bounded Context 或团队间有上下游关系(单向依赖)时,上游⽅可以独⽴于下游⽅完成开发,⽽下游⽅必须受限于上游⽅的开发计划。当上游⽅开会或做决策时,下游⽅也需要被通知甚⾄⼀起参加会议。这种关系情况下上下游⼀般会有⼀整套的测试⽤例,⽤于维护相互之间的不变性。上游的更改只要能够满⾜约定的不变性,即可不⽤通知下游进⾏变更。例如:事件发布或 者消息队列等⽅式;
- 「Conformist(遵奉者)」 : 同样在 Bounded Context 或团队间有上下游关系时出现,但此时上游⽅没有任何 动⼒要满⾜下游⽅的需求,这种关係我们就称下游⽅为 Conformist。此关系下有可能出现上游变更后, 短时间内下游逻辑异常的情况,此时需要下游对新的变更进⾏适配。
- 「Seperate Way(另谋他路)」 : 当两个 Bounded Context 或团队间因为技术、沟通或政治因素导致合作成本 过⾼时,就可以考虑断开两者的依赖关系。这并不代表两者毫⽆关系,仍有可能透过 UI 或是⼿动的模式 来进⾏整合限界上下⽂不仅是业务语⾔和领域知识的边界,也是团队合作的边界,清晰地定义不同上下⽂的关系不 仅有助于梳理业务之间的关系,也有助于理解不同团队的合作关系。
以终为始,道法⾃然
唠了这么多,其实我们反复在讨论的只有⼀个问题,「那就是DDD本身并不可怕」,可怕的是⽣搬硬套所谓最佳实践以及对⽅法要解决的问题本质分析不透彻就硬上,这最终会对我们的系统造成反噬。
即便对DDD整套理论没有系统全⾯的认知,如果你始终秉承着⾯向对象设计、⾯向对象建模的思路,通过 「核⼼模型领域建模 + 结构化编程思想 + ⾯向对象设计模式的合理使⽤」(针对问题域 适度封装与预留扩展点),我们写出来的代码质量的坏味道就会少很多。
简单的说,是不是DDD驱动的不重要,重要在于够不够OO,⾜够OO,⾜够健壮,是否DDD已经不再重要,我们只需要从DDD中适度的借鉴采⽤适合我们的⽅法/⼯具,达成软件的落地就可以。
所谓的⼤象⽆形,融汇贯通就是这样的,(就好⽐:张⽆忌当初练习太极剑,从开始到结束,忘记了所有招数, 达到运⽤⾃如,融会贯通的化境。)DDD对于你完全成为⼀套⼯具箱。
⼀切的⼀切都是为了 「“Get things Done”」,让事情是他本来的样⼦,让事情能够落地。这才是我们要使用DDD的价值,这亦是我所认为的DDD的⽬的。因为DDD本质上就是解决问题的一套思想的沉淀。
DDD推荐学习资料
如果想继续了解DDD,那么可以阅读以下资料,排名不分先后:
- 实现领域驱动设计
- 领域驱动设计精粹
- 微服务架构设计模式
- 极客时间《DDD实战课》
- Gitchat《领域驱动设计实践战略+战术》