一次让我想清楚很多事的系统重构

一次让我想清楚很多事的系统重构
踱鸽&水晶蟹一次让我想清楚很多事的系统重构
一、旧系统的状态
说”旧”其实不太准确,那个系统当时只跑了三年,但三年里业务扩展了好几倍,代码已经是另一种状态了。
症状很典型:一个”订单处理”的 Service 类,里面有 4000 行代码,涉及下单、支付回调、退款、对账、库存扣减。这些逻辑本来应该属于不同的域,但随着需求一个个加进来,全都堆在了这个文件里。
改一个需求要修改几个地方,改完之后不知道会影响哪里,上线前的自测范围越来越大,测试同学每次回归都说”你这次改了什么,我要全测一遍”。
有一个痛点最直观:产品提了一个需求,要支持”先货后款”的付款方式,和原有逻辑有交叉。我评估工时写了”5天”,组长皱了皱眉说”有那么复杂吗”——但不是因为我在虚报,是因为我真的搞不清楚改动的影响范围。
二、改造的动机
重构这件事是我主动提的,时间节点大概是 2022 年初。
触发点是一次需求评审,产品提了一个新的业务方向——多渠道订单聚合,简单说就是把不同来源的订单统一进来。我在脑子里过了一遍,发现要改的地方太多了,而且那些地方都互相依赖,我说不清楚改完之后系统还是不是稳的。
我提了重构方案,理由是:我们现在的系统结构已经无法支撑接下来的业务需求,与其接下来每个需求都用打补丁的方式堆上去,不如花时间把基础做好。
组长的第一反应是:”现在系统稳定运行,重构的风险怎么控制?”这个问题是合理的,也是重构最难说服人的地方。
三、方案设计的核心考量
方案最终选择了按业务域做模块分拆,不是上来就拆成微服务,而是先在单体内部把边界划清楚。
核心考量有三个:
一、不能停服迁移。 线上订单量每天几万条,不能像装修一样关门施工,新旧代码要并行运行。我们选择了双写过渡——新的逻辑写在新模块,旧代码先不删,新模块灰度跑稳了再逐步切流。
二、约束条件先于技术方案。 最开始有人提 DDD,有人提 CQRS,讨论了两次发现大家对这些概念的理解不一样,不能作为对齐的语言。最后我们用了一个更朴素的方式:画领域边界图,每个域有自己的 Service 层,跨域调用只通过定义好的接口,禁止直接调用其他域的 Repository。规则简单,才能执行。
三、数据库 Schema 不动。 这个决定救了我们很多时间。表结构变动风险高、迁移成本大,而且一旦出问题很难回滚。我们把重构的范围限制在代码层,数据层只做必要的索引补充。
四、过程中最大的阻力
技术层面最难的是状态机。原来的代码里,订单状态流转散落在各个 if-else 里,有些状态边界是通过注释来保证的,没有显式的约束。把这部分改成显式的状态机需要把所有隐含的状态边界找出来,花了整整两周做梳理,期间发现了三个原来代码里从未被测试覆盖到的状态转换路径。
人的层面也有阻力。有一个老同学明确说:现在系统跑得好好的,你这么折腾值得吗?我的回应是:不是为了折腾而折腾,是因为下一个大需求已经在路上了,再不动后面每个需求都得付双倍的代价。
最后说服大家的不是道理,是一个 Demo:我用新架构把一个小功能实现了一遍,拿给大家看——改动范围多小,边界多清晰,改完之后自测能快多少。
五、上线之后
历时大约四个月,主体逻辑迁移完成,旧的 Service 巨类逐步下线。
预判准了的:代码可维护性明显提高,新需求的评估时间和实际工时都变短了。之前那个”5天”的需求,类似复杂度的新需求现在大概 2 天能做完。
没有预判准的:测试覆盖的工作量比我想的多。因为原来很多逻辑是”约定俗成”的,没有测试用例,迁移过程中要补用例,这块花的时间比预想多了将近一倍。
如果重来会怎么做:第一步就把核心流程的测试用例补齐,然后再开始迁移代码。没有用例保底的重构,每次上线前都是靠胆量,不靠工程能力。

