arrow_back返回文章列表

重构为什么总被推迟:如何在交付中改善设计

一个新需求原本只要增加一种筛选条件,打开代码后却发现,列表、导出和统计各写了一套判断。修一处怕漏两处,统一起来又怕影响旧功能。最后的选择常常是再加一个分支,等“有时间”再整理。

下一次修改会更难,于是重构需要的时间也更多。它逐渐变成一项无法批准的独立工程。

Martin Fowler 在《重构》中讨论的是另一种工作方式:通过一系列小的、保持行为的转换,改善现有代码的设计。这个定义约束了修改范围,帮助我们把结构调整与功能变化分开。作者的定义

对日常交付来说,问题可以换成:为了完成眼前的需求,先做哪一点结构调整,能够减少后面的困难?

把“需要重构”变成一个可检查的问题

下面使用一个构造的订单列表场景。

系统已经支持按状态筛选订单。列表在内存里判断,导出使用数据库条件,统计接口又把状态转换成另一组代码。新需求要求排除已取消且超过保留期的订单。开发者需要同时理解三处规则,还要确认日期边界是否一致。

直接提出“重构订单模块”,很难解释完成标准。范围可能从条件提取一路扩大到数据层替换。更具体的目标是:让保留期规则有一个权威定义,并使列表、导出和统计能验证同一组边界样例。

这并不要求三种执行方式使用相同实现。内存判断与数据库查询可以保留各自形式,但日期和状态的含义要一致。究竟采用共享规则、查询对象还是契约测试,要根据系统能力选择;不能为了去掉几行重复代码,把分页数据全部拉到内存。

目标明确后,重构的收益也容易说明:本次需求将少掉哪些重复判断,后续哪些修改不再需要逐处对照。这比“代码更优雅”更适合进入交付讨论。

先调整结构,再改变行为

Fowler 在谈机会式重构时,提到可以先调整已有结构,使即将实现的功能更容易加入。他同时提醒,应在测试通过时进行这些调整。机会式重构

在订单示例中,可以先为现有筛选建立测试,再把散落的条件命名、提取,让原来的结果保持一致。确认这一步通过后,另一步才加入“超过保留期”的新规则,并补上相应测试。

这个顺序的价值,在失败时最明显。如果提取之后结果变化,问题来自结构调整;如果新增规则后变化,则检查新需求及其实现。把两者混在一起,评审者就很难判断某个不同到底是计划内变化,还是意外回归。

这里不一定要机械地把每次提取都变成一个提交,但变更应当能够分开审阅。修改已有公共接口尤其需要谨慎:替换内部调用和移除对外接口不是同一件事,后者可能涉及其他调用方与兼容承诺。

“行为保持”需要从使用者的角度检查

保持函数返回值并不足以说明行为没变。订单列表的默认排序、同一时间订单的稳定顺序、错误类型,以及空结果的展示方式,都可能被外部依赖。

移动端还有生命周期上的约定。把一段回调代码改成另一种异步写法,如果回调所在线程变了,页面可能出错;取消请求之后是否还允许完成回调,也会影响已关闭页面的状态。这些都应先被识别,而不能在重构完成后才当作小问题补救。

性能同样需要检查。保持输出,却把一次查询变成每条记录一次查询,仍可能破坏使用者能够接受的运行条件。正确做法是保留相关预算并做针对性验证,不必为了一个条件提取跑遍所有性能测试,也不能只看结果相同就结束。

因此,开始之前要列出这次修改触及的可观察行为:输入输出、错误与副作用,必要时包括时间、资源和调用顺序。范围越具体,保护它的测试越有针对性。

测试不能只证明新结构存在

一种脆弱的测试会断言内部方法调用了几次,或者新类是否被创建。它们可能在每次结构变化时失效,却没有指出订单结果是否错误。

对于筛选规则,更有用的样例是:恰好处于截止时刻、时间略早、时间略晚;取消与未取消;缺少日期时已有系统怎样处理。测试表达这些行为,内部条件怎么组织便有调整空间。

测试也不是绝对保证。某处存在测试,不代表所有消费者的需求都已覆盖。如果导出格式受到外部工具依赖,就应保留相应结果验证。新增测试的范围应跟着实际风险走,不能以覆盖率达到一个数字代替判断。

没有任何测试保护时,也不必直接进入大改。可以先从公开入口记录少量稳定行为,再做最小调整。若为了建立测试必须先改动复杂依赖,应把这一步的风险单独说明,并缩小验证边界。

重构需要结束条件

整理代码容易让人顺手解决更多问题。筛选条件提取之后,又发现命名不统一;改完命名,又想升级数据框架。每一项都有理由,合起来却让本次需求迟迟无法交付。

本文建议用当前修改的障碍确定范围。示例中的结束条件可以是:新规则能够在明确位置加入,三类消费者对边界样例的结果一致,原有行为测试通过。其他问题记录下来,不在这次变更里处理。

如果障碍来自系统性问题,确实需要独立安排改造。例如数据模型无法表达新状态,或发布兼容要求使小步调整不足以完成目标。此时应按迁移工程说明阶段、兼容策略和失败恢复,而不要继续用“重构一下”掩盖规模。

同样,替换一个即将下线、几乎不再修改的模块,可能没有足够收益。重构改善的是后续理解和修改的成本;没有后续用途时,把有限时间用于测试、隔离或退役,也可能更合理。

在下一次需求里尝试小一点

收到需求后,可以先写下最阻碍修改的一件事。例如“同一个规则需要改三处”,或者“业务计算和数据库读写混在一起,无法单独验证”。再问:是否有一个保持现有行为的调整,能先解除这个障碍?

若有,就为它定义保护样例、审阅边界和结束条件,然后再实现需求。如果调整的成本明显超过本次收益,把原因和后续机会记下来。这样推迟重构也是经过判断的选择,而不只是默认再堆一个分支。

代码不必先被改造成理想架构,才有资格承接新功能。交付过程中持续减少下一步的困难,才是这套方法能长期使用的原因。

flare

保持好奇,继续思考。