一次后端重构的经验:从混乱代码到清晰模块

📅 发布时间:2026/8/27 7:59:14
一次后端重构的经验:从混乱代码到清晰模块 接手那个服务的第一天我对着屏幕上两千多行的GodController发愣。路由层直接操作数据库业务逻辑里混着SQL字符串拼接甚至还有一段用正则解析XML配置的代码写注释的人早已离职而那段逻辑每周都会跑崩一次。重构不是技术洁癖的发作而是每次上线都像在雷区里跳舞——你不知道哪次改动会引发连锁爆炸。后来我花了六周把这个项目从“哪里都能改哪里都敢炸”的状态梳理成一张清晰的分层地图。这段经历没有高深算法全是糙活、脏活以及无数次对“如何拆解复杂”的思考。第一步承认混乱是常态而不是异常代码变质不是某一天的突然堕落。需求像藤蔓一样缠绕在原有结构上每个人都带着“顺手改一下”的善意却导致每个类都承载了三种职责。我的第一个动作不是重写而是把所有“不敢动”的代码先圈出来标注它们为什么让人恐惧。恐惧通常来自不确定的副作用某个静态类保存了全局状态某处隐藏的定时任务依赖数据库里无索引的查询。我建了一个“危险清单”把每次故障的根因、触发条件、关联模块都记录下来。这个过程让我意识到重构最大的阻力不是技术债而是对未知的恐惧——只要还存在任何一个“改了会炸但不知道为什么会炸”的角落团队就只能继续在泥潭里打补丁。于是我先做了一件事给所有关键路径加日志和指标。不是那种logger.info(“处理完成”)的无意义日志而是记录入参、出参、耗时、异常堆栈的完整追踪。当你能实时看到每一次请求经过哪段代码、花了多长时间、哪一步抛异常时混乱才第一次变得可观测。可观测性是重构的前提没有数据支撑的改动都是赌运气。从“改一处动全身”到“按边界切一刀”真正的重构从划分边界开始。我尝试用“封装变化”的思路把服务按业务能力拆成几个独立的模块——用户、订单、支付、通知。听起来像教科书对吧但执行起来非常痛苦。原来的代码里用户模块会直接查询订单表订单模块又会反向更新用户积分。边界不是画出来的是断出来的——你必须在每个越界访问的地方做一次抉择是迁移数据归属还是暴露一个接口。我选择了最保守的做法先不破坏现有功能而是把所有跨模块访问改成通过内部API调用。比如订单模块需要用户信息不再直接查user表而是调用UserService.getById()。这个过程中的阻力来自同事的质疑“这样不还是内部调用吗和直接查表有什么区别”区别在于你给模块之间装上了感知得到的门。直接查表是穿墙而过内部API是走正门。一旦装上这门后续就能在门口加缓存、加权限校验、加事件通知。更重要的是当某个模块的底层表结构要改时影响范围被限制在那个模块内部而不是波及整个依赖网络。两周后我们第一次实现了“只改订单模块代码而不动其他模块”的上线。那个版本发布后监控曲线平稳得像一条死线团队群里安静了三分钟然后有人打了一行字“原来上线可以这么轻松。”那些藏在地毯下的“神隐代码”拆模块时我遇到一个极端案例有一条支付回调的入口它先从请求头里取一个自签名参数然后校验有效期内是否被调用过再拼接一串加密字符串去反查数据库里的流水号。整个过程没有任何注释方法名叫process参数是两个String。我花了整整一个下午靠全局搜索调用点、翻Git历史、查看测试用例根本没有才拼凑出它的真实作用——用于处理第三方支付的重试通知。最可怕的代码不是写得烂的代码而是没有人知道它到底在干什么的代码。我把这类“神隐代码”单独拉出来建立了一个“技术债清算表”。表里每条记录包含这段代码的真实用途、当前依赖方、风险等级、建议重构方式。有的直接删除历史遗留的死代码有的提取成独立服务比如那个支付回调有的则重写为明确命名的函数。给代码准确的命名就是给它一个合法的身份。当process变成handlePaymentRetryNotification后即使内部逻辑再复杂后续维护者至少有了一个正确的起点。重构不是把所有代码都写成最优解而是让每一个存在的代码都能被追溯到它存在的理由。依赖反转把“高层依赖低层”倒过来分好模块后我发现一个新的混乱源业务层直接依赖具体的第三方SDK或工具类。比如订单服务里到处是HttpClientUtil.post()支付模块里直接new了一个OkHttpClient通知模块依赖短信渠道商的API类。一旦供应商要升级SDK或者切换渠道商就要在全部调用点做改动。这种依赖关系像地底下的电缆表面上整齐实际上缠成一团。我用依赖注入和接口定义来扭转局面。给每个外部服务定义一个“端到端”的业务接口例如SmsSender、PaymentGateway、InvoicePdfGenerator。业务层只依赖这些接口具体实现放在一个独立的infrastructure适配层。这个过程听起来简单实际却需要勇气——因为你要敢于对现有的调用方式说不敢于为了一个接口去改动几十个文件。但收益立竿见影我们把短信服务从阿里云切换到腾讯云业务层零改动只是替换了一个Bean实现。架构的优雅不在于设计时的华丽而在于变更时的那种风轻云淡。测试重构的安全网也是重构的地图如果不提测试任何重构经验都像只穿了一只鞋。我们项目原有的测试覆盖率为8%关键路径几乎没有。重构的第一步我要求团队把每个模块最核心的三条成功路径和两条异常路径写成集成测试。不是为了追求覆盖率数字而是为了让重构过程中每次改动都有一面镜子。测试不是在验证代码正确而是在记录当前行为——有了这些记录你才知道自己是否破坏了什么。写测试的过程本身就能暴露混乱。如果一个函数难以测试通常意味着它耦合了太多东西。比如一个pay()方法内部直接读取配置文件又写数据库还调外部API——为了测试它你得Mock掉一切。我拆出第一步时把“可测试性”作为设计的硬指标每个方法要么是纯函数要么只依赖注入的接口。当这个指标慢慢落实代码的结构自然趋向于清晰。重构后期我们跑一次完整测试只要4分钟但正是这4分钟让团队敢于双击文件删除几十行废代码而不手心冒汗。删代码比写代码更需要勇气清理死代码是重构里最被低估的部分。我统计了一下重写后的服务里大约有35%的代码是原系统留下的“僵尸代码”——从未被调用但所有人都知道它们的存在。为什么没人删因为怕删错了怕某个反射或动态调用偷偷引用它。我们花了几天时间给所有方法加了调用统计日志在灰度环境跑了一周然后把零调用的方法全部移进一个deprecated包再跑两周确认无影响后彻底删除。删掉一万行代码的爽快感远远大于写一万行代码的成就感。但更微妙的是“看起来有人用其实没人知道怎么用”的代码。我们有一个Excel导出工具类支持十几种格式配置实际产品只用一种。它本身写得并不烂但它的存在让新人在“用工具类”和“直接构造对象”之间犹豫不决产生了大量重复的包装层。每一个不必要的抽象都是一种认知税——你每多读一层“可能有用”的封装就损失一次理解核心逻辑的机会。所以后来我们形成一条铁律新代码若超过两周未被使用就移出主干目录。清晰不是靠堆积设计而是靠持续修剪。模块稳定之后再谈性能优化重构进行到第四周模块已经能独立部署、独立测试。这时我们才开始做性能优化。在混乱阶段做的性能优化往往只是给怪物穿上更快的靴子——比如把某个频繁查询加个缓存但没解决查询本身绕了三个模块。等到模块清晰后性能问题变得一目了然某个订单列表接口先循环调用用户服务获取用户名N1再调支付服务检查状态又N1最后拼装返回。在清晰的结构下我可以直接给用户服务加一个批量查询接口把N次网络调用改成一次。清晰的结构不会让性能自动变好但它会让性能瓶颈显形。以前面对一个慢接口你知道是慢查询是序列化是网络延迟还是业务算法太复杂只能猜。现在你能顺着调用链一层层看下去每一步的耗时都记录在案。有一次我们发现一个“统计当日营业额”的接口慢到8秒顺着调用链发现竟然是循环调用一个“获取全部订单”的接口然后在内存里做筛选。业务代码没变只是结构从“数据库里算”改成了“服务里算”但改完结构后这个错误就无处躲藏了。性能问题的本质多数时候是结构问题。重构期间如何活下来兼容、灰度、回滚重写与重构不同尤其在后端服务里你不能让业务停下来等代码好转。我们的策略是“蠕动式重构”每次改动都不改变对外接口不改动数据表结构只是内部模块重组。所有重构分支都独立合并到主干但保留一个“兼容层”开关——如果新模块出了哪怕一点异常可以通过配置中心一键切回旧逻辑。这个开关在重构期间救了我们三次一次是缓存同步逻辑漏了更新事件一次是分布式锁的key命名不统一最后一次是新模块对多租户数据的过滤条件写反了。每次回滚都让我们反思“为什么测试没发现”然后补上对应的回归用例。重构失败不可怕可怕的是失败了回不去。所以每完成一步我们都打一个可部署的标签记录这次改动涉及哪些模块、哪些依赖、哪些环境变量。项目重构到第五周时主干的编译时间从90秒降到25秒启动时间从40秒降到12秒。这个数字变化直接提升了团队的迭代频率——你不再因为“改代码要等半天”而拖延小修小补。开发体验的改善最终会反馈到代码质量的改善上因为人们更愿意在小问题刚出现时就修掉它。重构真正结局不是代码变好而是团队敢于改代码现在回想那六周让我印象最深的不是最后的架构图有多么漂亮而是重构第三周的一次普通代码评审。一个新同事提交了一个改动要在支付模块里加一个“根据金额四舍五入”的逻辑。放在以前他能找到的位置可能有三处一个散落在工具类里的roundMoney一个在订单实体里的getTotalAmount还有一个藏在PaymentController里的私有方法。而现在他很自然地打开MoneyUtils写完测试然后退出。他不需要问“这个逻辑我该放哪”因为模块的边界已经把答案写出来了。好的架构不是让聪明人更容易写好代码而是让普通人不容易写坏代码。当你的团队不需要靠记忆和默契来维持模块纪律而是靠代码本身的走向就能做出正确选择时重构才算真正结束。那些看起来枯燥的分层、接口、依赖规则最终变成了一种无声的引导——它不告诉你怎么做只告诉你哪里容错、哪里严谨、哪里需要小心。直到这时候混乱不再是一种宿命而是一种可以随时被修正的状态。于是我终于明白重构的本质不是把旧代码变成新代码而是把“不敢动”变成“敢动”。这个转变远比任何技术方案都来得金贵。