Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。
好久没有写过文章了,上一次还是签约作者的赶稿。稿子赶完之后,每天的糟心事太多了,一直提不起兴趣来创作。
不过快到年底了嘛,还是给自己空了一年的技术创作做个虚假的填补吧。
22年年初出了DDD小册之后没多久我就进了外企工作,这一年多的时间其实也没啥大的技术成长与挑战,大多都是一些普通的不能再普通的业务需求开发。
相对来说比较有成就感的事情就是完成了部门系统的DDD升级改造。在进入外企之后的一个半月,我们部门就成立了一个专门的小组来计划对现存的老项目进行领域划分与DDD改造。
本文将针对我在外企的DDD改造的心路历程与技术历程做一个总结归纳。
本文不会介绍DDD相关的基础概念(比如领域,仓储的含义),对于DDD基础概念还不是很清楚的小伙伴可以试读一下我的小册或者翻看一些我的历史文章。
DDD系列博客
掘金小册《深入浅出DDD》
一、架构升级的历程
我在写小册之前,上家公司我就完成了DDD的架构迁移。不过那个时候更多是因为自己想做技术提升,并且领导也给了机会去做,其实当时我搭建的mvc架构在功能迭代的时候表现的也很出色,DDD的系统改造升级并不是一个强需求 (系统的业务结构与组成总体来说比较简单,难点主要在于一些技术实现上) 。
架构升级这种事情,大多都是为了高绩效才会去做的事情。对于大多数的程序员而言,开发历史项目时,多多少少都会因为历史代码里面的那些屎山逻辑而骂骂咧咧。但是在屎山上开发的时候,我们都会选择在上面再拉一坨(加一个if就完事儿),不会选择洋洋洒洒的去重构历史逻辑。
根本原因在于:可以但没必要。 本身你去开发一个需求的时候,一个if可以解决的问题,你为了让它可以变得更好的迭代,重构了它,但是导致了一些不可预知的问题,反而是你的不是了,吃力不讨好。
到了外企后,我参与开发的系统已经上线了4年多快5年的时间。期间经手的人很多,大家都以MVC结构进行开发。因为MVC架构开发的灵活性很高,每个人都有每个人的编码风格,久而久之,项目体积庞大,一个简单的业务逻辑变更,可能会牵扯到很多代码的改动。
当时我们面临开发困境:
- 系统庞大,经手人员多
- 业务持续迭代中逻辑混乱,容易出现bug
- 编码风格混乱导致业务出入口不一致,维护困难
为了改变上述可持续迭代的 、复杂业务逻辑的 业务系统的开发困境,引入DDD去统一业务语言,从业务出发重新审视架构。
对于外企成熟产品的功能迭代风格而言,宁愿上线慢,不愿上线错。 所以我们在架构升级改造、头脑风暴、领域划分、职责分工上花费了很长的时间(从22年的5月份到23年的3月份左右)。
当所有的前期准备工作结束,我做出第一版架构迁移的接口Demo后,让部门里面的小伙伴了解DDD,并且能够照着我给出的Demo进行开发又花费了一个多月的时间。最终部门小伙伴10几个人花费3个多月的时间加班加点改造出一个基于DDD的雏形架构。期间我还要持续跟进代码规范把关小伙伴们的代码规范,培训+持续改进。
耗时将近一年半的时间,完成了系统架构从MVC到DDD的迁移,成本非常之大。但是好处也是很明显的,我们现在所有的业务都有了统一的规范与出入口,功能迭代更加丝滑。
二、流量切换
业务架构升级这玩意儿,说破了天,都是代码层面的结构优化。从业务流程来看,它是没有任何变化的。简而言之,你所有的迁移操作都不能对用户造成影响。
最近一段时间的滴滴崩盘、阿里云崩盘、语雀崩盘都是降本增笑的典型案例,我们可不能在优化系统的时候把自己给优化了。
言归正传,流量控制需要考虑哪些条件呢?
1.用户/接口白名单测试:
上线的时候我们需要针对已经完成迁移的接口使用白名单账号来进行测试
2.用户/接口灰度测试:
测试账号测试完成之后肯定不可能一下对用户全开放,流量肯定是逐步切换,5%的流量开放给新接口,95%流量留在老接口。从新接口的流量的日志检测是否有问题。
3.用户/接口黑名单测试:
假设我们有100个迁移的测试接口都是以/api/baiyan开头,流量切换的时候我们配置了/api/baiyan/**。将所有以/api/baiyan开头的流量都打到了新接口上,但是偏偏/api/baiyan/test这个接口出了问题,其他都正常,那这个时候我是删除这个通配符的配置呢?还是将99个正常接口一个个加到流量的配置上呢?我们需要对错误的api设置黑名单。
流量控制的优先级
黑名单>白名单>灰度名单
按照优先级:
如果用户命中黑名单策略,将请求路由到老代码。
如果用户没有命中黑名单,命中了白名单,将请求路由到新代码。
如果用户没有命中黑名单与白名单,命中灰度名单,将请求路由到新代码。
如果均不满足上述名单,请求路由到老代码。
流量切换如何实现呢?
1.网关层
如果你们的系统存在统一的网关,nginx接口请求后转发到网关进行鉴权与限流。可以在网关增加一个filter,根据流量控制的优先级来路由实现。
2.单体项目
如果你们的系统没有使用网关,nginx之后直接到项目,那么在项目中增加filter,根据流量控制的优先级来路由实现。
三、协作开发的优先级
迁移工作因为是部门内的小伙伴一起完成的,那么必然会存在功能分工。而功能与功能之前都会存在互相依赖与共用的模块。如果架构迁移后的代码在新服务中,存在A功能依赖B,但是B现在没人参与开发。
这个时候该怎么呢?总不能等B开发完成我再开始我的代码改造。
并行开发的情况下,开发A功能的同学对于自己所负责部分的功能代码以外的所有依赖都通过adapter调用rpc的形式调用老服务来实现。等到B代码迁移完成,A的adapter中对于B的rpc逻辑可以切换成对B代码的调用。
这样就可以互不影响,各自开发。
原则上所有有依赖的代码逻辑都可以使用adapter调用rpc的形式防腐做掉。各自迁移各自的,但是如果都是这样的话adapter就会很多,哪怕后续B迁移完成,A对于B的调用也都会在adapter中转。
所以代码的迁移的原则上,工具类,基础服务,基础功能先设计迁移,再迁移上层业务模型,避免一些不必要的防腐层代码。对于一些迁移不动的历史代码或者实在没有资源迁移的逻辑,使用防腐层包装。
主打一个能救多少是多少!
四、数据双写
因为有了仓储的存在,领域模型与数据模型之间的转换得到了一层保护。
但是往往我们都会选择开辟一套存储表来隔离新老项目,为什么?
老项目中所有的代码逻辑都是基于数据模型的,DDD架构下的代码逻辑都是基于领域模型的。
如果新老项目共用同一套数据,那么当新项目中如果出现了写数据的代码问题。这时,你将流量从新服务切换到老服务时,bug仍将存在,是一个假性的流量切换。
因此对于DDD架构的系统而言,我们会根据领域模型重新设计数据模型。
一来是保护了老项目的运行不受影响,出现异常时会滚能够保证用户功能正常。
二来能够让新项目迁移完成后数据模型更加切合领域模型,可视化效果更好。
新服务与老服务之间读写隔离的情况下要做好数据迁移与双写,要保证用户不会因为流量切换而导致数据不一致的情况。
五、分层与调用顺序
MVC架构中,为什么代码会乱?
因为M与V都是很薄的一层,而所有的逻辑与各种各样的处理都可以在C中体现。C变成了一个万能层,所有工作都干了,导致逻辑的重叠性很高。
当我们使用能力层方式 (小册中有介绍能力层的定义) 迁移DDD的时候,增删改都可以使用领域模型的方式进行建模,内聚业务,优化代码。但是到了读逻辑的时候,走CQRS,与原有的MVC分层的代码写法上差异并不是特别大。
所以在第一版迁移代码后,发现读逻辑基本没有什么变化,还是一坨混在了一起。而且从读写分离的角度来看,写逻辑依赖的一些配置或者基础数据与查询方法提供的代码高度重合。
在实际迁移的过程中,为了解决读耦合与写时配置load的问题,我们增加了两个分层。
1.query ability
它的定位与cmd ability类似,因为在增删改的逻辑中我们无法去调用query application service,对于在增删改与查询时共用的逻辑我们使用query ability来中转。query ability就是一个原子化的业务case数据,可以是配置,可以是各种复杂计算查询的数据。
2.feature service
它的定位有点类似于读的domain,对于不同query ability共用的逻辑,我们通过feature service来包装。
旨在单一功能,最小化。
调用顺序如下
原则很简单,整个项目中不要存在重复的代码逻辑,所有的业务逻辑最终都能有一个收口的地方。
六、总结
其实吧,正儿八经的将一个庞大的系统从MVC迁移到DDD是一个非常损耗人力与时间的操作。而且DDD架构并不适合所有的业务系统。
对于中小型,项目化的工程,业务需求可能一天一个样。花费巨大的成本去迁移,转头项目被毙了或者你刚建立的业务模型,PM说改就改了,那真是得不偿失。还不如借用一点DDD的思想,做一些项目上适当的包装,在方法定义的时候尽量最小化,提高复用性。
对于业务需求稳定的、可持续迭代的大型工程,如果决定了要做DDD改造。一定要全面评估好架构切换可能会面临的那些问题,毕竟代码结构调整可能简单,但是调整后的坑可能就不简单了。
七、联系我
如果你觉得文章写得不错,点赞评论+关注,么么哒~
微信:baiyan_lou
掘金小册《深入浅出DDD》已在掘金上线,欢迎大家试读~
DDD的微信群我也已经建好了,大家可以加我微信,备注DDD交流,我拉你进群,欢迎交流共同进步。