58.7 重构三式(有的放矢 / 合纵连横 / 运筹帷幄)

一句话

重构不是 “重写”,是在飞行中换引擎:先用判据筛掉不该重构的问题(有的放矢),再用数据和事实争取到人和时间(合纵连横),最后分段落地、每段 1–3 个月可交付(运筹帷幄)——三式缺一式,重构就变成一场没有终点的加班。

先看清:重构为什么会失败

不是技术不行,是排序与协作不行。三个高频死因:

死因 表现 后果
不分优先级 100 个问题一起上,摊大饼 半年后每个问题都做了一半
不按性质分类 救火、优化、重构混在一个迭代里 方案反复,返工率极高
被版本压力逼着挑软柿子 先做 “好做的”,难做的永远排不上 核心问题拖到最后,变成事故驱动

🔑 重构的失败几乎都不是 “代码写不出来”,而是 “选错了要解决的问题” + “拿不到连续的时间”。三式分别对应这三件事。

第一式:有的放矢——先证明 “必须重构”

判据:从 0 设计,新老架构差得多吗

假设从 0 设计的结果 该做什么 成本量级
新架构与老架构差不多 只做系统优化(加索引、加缓存、调参数) 周级
新架构与老架构差很多 才值得重构(改结构、拆服务、换存储) 季度级

这条判据能挡掉一半的 “重构冲动”:多数时候你恨的不是架构,是慢查询。

第一步动作:把 100 行筛成 50 行

典型起点是一张 “大家都知道有问题” 的清单(Excel 里能写到 100 行)。筛法是只留 “不重构就解决不了” 的:

筛掉 理由
加个索引/缓存/参数就能缓解的 属优化,不属重构
已经被业务下线或不再迭代的模块 改了也没人受益
只影响内部工具、不影响用户的 优先级天然靠后
说不清 “不改会怎样” 的 没有靶心,无法验收

两个真实收益(原文案例,用来给重构 “报价”):

案例 动作 结果
M 系统 解耦 每月上线版本数是原来的 4 倍
S 系统 双中心 可用性 3 个 9 → 4 个 9;此前最差一个月发生 4 次大故障

技术债怎么记账与排序见 58.2 技术债管理。

🔑 “有的放矢” 的产出不是方案,是一份被削减过的清单 + 每条的量级收益。没有这两样,第二式无从谈起。

第二式:合纵连横——把技术问题翻译成业务问题

合纵:讲人话

技术词对业务方没有说服力,要换成他们能感知的损失:

你想说 换成
“可扩展性差” “改都改不动:一个需求要动 5 个模块,任何改动都要全量回归”
“可用性只有 3 个 9” 摆数据:“上个月 4 次大故障,累计不可用 6 小时,按 GMV 折算是 XX 万”
“架构耦合严重” “这条需求排队 3 周,其中开发 2 天、等联调 2 周半”

一句话模板(原文口径):“一个月只能做 4 个版本;有版本讨论 1–2 周,开发只要 2 天;最极端还要再等 1 个月,跟门户一起上线。” 这比任何架构图都能打动人。

连横:处理两个真实阻力

阻力 潜台词 有效做法 无效做法
“对我有什么好处?” 你的重构要花我的排期 换位思考:把他的 KPI 和这次改动绑在一起(例如少一次故障 = 少一次半夜起来) 谈 “公司利益”“技术理想”
“我这边现在不急” 优先级排不上 给出明确的依赖与截止时间,让对方看到 “晚做会更贵” 说 “以后有空再做”

三条硬规则:

规则 做法
时间必须写死 写 “3 个月后”“6 月上线”,不许写 “以后”“等不忙的时候”——后者等于永不
真对某组不利就上升一层 不在会上互相消耗,把取舍交给更高一层决策
排不开就分阶段 先做别的系统,把依赖解开再回来;不要硬塞进同一个版本

第三式:运筹帷幄——分段落地,每段可交付

三段:救火 → 优化 → 重构

顺序不能反:救火在前(先让系统活下来),优化在中(用最小代价拿收益),重构在后(结构性问题才动刀)。

段 目标 典型动作 验收
救火 止血 扩容、加连接池、治慢查询、加缓存、Nginx 一键切换 故障次数/时长下降
优化 用最小改动换最大收益 切公共组件、统一超时重试、缓存治理 关键指标改善
重构 改结构 拆服务、库从单点改多中心、模块解耦 上线频率/可用性提升

真实案例(X 项目 / S 系统):先把 “过载、缓存慢、虚拟机挂死” 救下来,再切公共组件,最后才做服务化——不先铺垫,重构方案会不聚焦。

分段四步:优先级 → 分类 → 先易后难 → 循序渐进

① 优先级:明显且紧急的先做(扩容永远排第一)
② 按性质分类:救火 / 优化 / 重构 各自成组
③ 组内先易后难:先用小胜建立信任,再啃硬骨头
④ 循序渐进:每段 1–3 个月,超了就再拆

一个能跑的规划器(本机实测,Python 3.14 / macOS arm64):把 10 条问题按上述规则自动排成阶段。

ITEMS = [  # (名称, 类型, 影响 1-5, 紧急 1-5, 周)
    ("核心库单点,主库挂即全站不可用", "救火", 5, 5, 3),
    ("大促前扩容(机器 + 连接池)",     "救火", 5, 4, 2),
    ("慢查询打满 DB,接口 P99 3s",     "救火", 4, 5, 2),
    ("把日志/监控切到公司公共组件",     "优化", 3, 2, 4),
    ("接口超时与重试参数统一",         "优化", 3, 3, 3),
    ("缓存 key 规范与容量治理",        "优化", 2, 2, 4),
    ("订单服务从单体拆出",             "重构", 4, 2, 10),
    ("用户中心服务化",                 "重构", 3, 1, 8),
    ("前端 BFF 收敛",                  "重构", 2, 1, 6),
    ("换一个更漂亮的监控大屏",         "优化", 1, 1, 4),   # 会被打回
]
PHASE_LIMIT = 12   # 周:一阶段最长 3 个月

kept = [x for x in ITEMS if x[2] >= 2]          # ① 筛掉影响=1 的问题
dropped = [x[0] for x in ITEMS if x[2] < 2]

order = {"救火": 0, "优化": 1, "重构": 2}
phases, cur, weeks = [], [], 0
for typ in ("救火", "优化", "重构"):            # ② 按性质分段
    for x in sorted([i for i in kept if i[1] == typ], key=lambda i: (-i[3], -i[2], i[4])):
        if cur and weeks + x[4] > PHASE_LIMIT:  # ④ 每段 ≤ 12 周
            phases.append(cur); cur, weeks = [], 0
        cur.append(x); weeks += x[4]
    if cur:
        phases.append(cur); cur, weeks = [], 0

print(f"候选 {len(ITEMS)} 条 → 保留 {len(kept)} 条,打回 {len(dropped)} 条: {dropped}")
for i, ph in enumerate(phases, 1):
    print(f"阶段 {i}({sum(x[4] for x in ph)} 周,{ '/'.join(sorted({x[1] for x in ph})) })")
    for x in ph:
        print(f"   [{x[1]}] {x[0]:<22} 影响 {x[2]} 紧急 {x[3]} 工期 {x[4]} 周")
print(f"校验:阶段数 {len(phases)},最长 {max(sum(x[4] for x in p) for p in phases)} 周(上限 {PHASE_LIMIT})")

真实输出:

候选 10 条 → 保留 9 条,打回 1 条: ['换一个更漂亮的监控大屏']

阶段 1(7 周,救火)
   [救火] 核心库单点,主库挂即全站不可用        影响 5 紧急 5 工期 3 周
   [救火] 慢查询打满 DB,接口 P99 3s     影响 4 紧急 5 工期 2 周
   [救火] 大促前扩容(机器 + 连接池)        影响 5 紧急 4 工期 2 周
阶段 2(11 周,优化)
   ...
阶段 3(10 周,重构)
   [重构] 订单服务从单体拆出              影响 4 紧急 2 工期 10 周

校验:阶段数 5,最长 11 周(上限 12)

注意两个细节:“换个漂亮的监控大屏” 因为影响面只有 1 被直接打回(这就是筛的动作);10 周的订单拆分独占一个阶段(因为它加进任何 8 周以上的阶段都会超 12 周)。

阈值:每阶段 1–3 个月,规划超 2 年就别规划了

判据 数值 含义
单阶段最短 1 个月 再短没有可见成果,团队会失去信心
单阶段最长 3 个月 超过就再拆,否则中途需求一变就全盘重排
整体规划 若估算需要 2 年 原文判断:不如直接重写

生产落地:指名 → 可抄 → 可验 → 可退

指名:问题清单用 Excel / 飞书多维表格(或 GitHub Projects)维护,一列一个属性;每月一次 30 分钟重构评审,只做三件事:加新问题、调优先级、确认本阶段还能不能按期交付。协调时间用版本排期表(谁在等谁,一眼看到)。

可抄(清单表头,直接建表):

编号,问题描述,类型(救火/优化/重构),影响面1-5,紧急度1-5,工期(周),依赖,owner,目标阶段,状态

可验:四个检查点,缺一条就说明 “在做但没章法”。

检查 合格线
每个阶段有明确截止日 写 “3 个月后” 而不是 “以后”
每阶段 ≤ 12 周 超了就拆,不靠加班硬扛
每条有 owner 没有 owner 的问题会永远排在最后
每阶段结束有可量化收益 例如 “上线频率 ×4”“可用性 3 个 9 → 4 个 9”

可退:重构的退路不是 “回滚代码”,而是缩小目标。三个可接受的降级:① 只做救火与优化,不做重构(很多团队的正确选择);② 把大重构拆成 “只解耦一条链路”;③ 冻结新增需求,给重构一个完整窗口(最后手段,代价最大)。

反模式

反模式 后果
100 个问题一起上 半年后每个都做了一半,没有一个可验收
没有验收判据就开工 做完说不清 “到底改善了没有”,下一次要预算更难
跟业务方讲 “可扩展性、耦合度” 拿不到排期,问题被无限推后
时间写 “以后有空再做” 等于永不做
把重构塞进业务版本里 “顺手做” 需求一变就全盘回滚,白干
一个阶段排 6 个月 中途任何变化都导致重排,团队失去信任
先挑软柿子 核心问题拖成事故,最后被动救火
重构期间继续接大量新需求 两边互相拖,最后两边都没交付

三式铁律

🔑 有的放矢:先证明 “不重构就解决不了”。从 0 设计、新老差不多的问题,只做优化。

🔑 合纵连横:把技术词翻译成损失数字,把时间写死。“改都改不动”“一个月 4 个版本、开发只要 2 天” 比架构图有用。

🔑 运筹帷幄:每阶段 1–3 个月、可交付、可验收。规划超过 2 年,不如直接重写。

自检

Q1 什么情况下 “重构” 其实是过度反应?用什么判据判断?

▸ 答案:当从 0 设计的新架构与老架构差不多时,你恨的不是结构,是性能或参数。判据就是这一条假设推演:如果重画一遍也长这样,那该做的是加索引、加缓存、调连接池这类系统优化(周级成本),而不是重构(季度级成本)。

Q2 为什么 “有的放矢” 的第一步是把 100 行问题清单砍到 50 行?

▸ 答案:因为重构的资源永远是稀缺的,而问题清单里混着大量 “优化能解决” 和 “已经没人用” 的项。砍掉的每一行都换来后面阶段的一次聚焦;不砍,就会出现 “每个问题都做了一半”。砍的判据是**“不重构就解决不了”**,不是 “看起来严重”。

Q3 向业务方争取重构排期时,为什么不能说 “可扩展性差”?

▸ 答案:因为这是技术语言,业务方无法折算成损失。有效的说法是把技术问题翻译成他们能感知的成本:“改都改不动,一个需求要动 5 个模块、全量回归 2 周”“一个月只能做 4 个版本,讨论 1–2 周、开发 2 天,最极端还要等门户一起上线 1 个月”。

Q4 面对 “对我有什么好处” 和 “我这边不急” 两种阻力,分别怎么处理?

▸ 答案:前者要把重构和他的 KPI 绑定(少一次故障 = 少一次半夜起来),不要谈 “公司利益”;后者要给明确的依赖和截止时间,让他看到 “晚做会更贵”。两条共同点:时间必须写死(“3 个月后”),真排不开就上升一层或分阶段,不要在会上互相消耗。

Q5 为什么顺序是 “救火 → 优化 → 重构”,而不是直接重构?

▸ 答案:因为重构周期长、见效慢,而系统的问题还在持续发生。先救火让业务活下来(扩容、治慢查询),再用优化拿快收益(切公共组件、统一超时),最后才动结构。原文案例里 X 项目与 S 系统都是这个顺序;反过来先动结构,往往做到一半就被线上故障打断。

Q6 单阶段为什么限定 1–3 个月?如果整体估算要 2 年怎么办?

▸ 答案:短于 1 个月看不到成果、团队失去信心;长于 3 个月,中途任何需求变化都会导致重排,且无法验收。整体若估算要 2 年,原文的判断是不如直接重写——因为 2 年的渐进重构,中间必然被无数次需求变更打断,最后既没重构完也没交付。

Q7 本机规划器把 “换一个更漂亮的监控大屏” 打回,用的什么规则?这条规则合理吗?

▸ 答案:规则是 “影响面 < 2 的问题不进重构计划”(影响面按用户/业务受损程度打 1–5 分)。合理之处在于重构的稀缺资源必须投给影响面大的问题;但如果这个需求本身有独立预算、不影响重构资源,它不该出现在这张表里——所以更准确的做法是 “不进重构计划”,而不是 “不做”。

关联