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 分)。合理之处在于重构的稀缺资源必须投给影响面大的问题;但如果这个需求本身有独立预算、不影响重构资源,它不该出现在这张表里——所以更准确的做法是 “不进重构计划”,而不是 “不做”。