设想你是一位中餐厅主厨。煎、炒、蒸、炖是你熟稔的烹饪技法,每一招都对应一种“设计模式”。单独一招能做出简单的菜,但一道真正的招牌菜,常常需要先煎后炖、边炒边蒸——技法配合得当,味道才惊艳。这就是“模式组合”:让多个设计模式像默契的厨艺一样协作。可日子久了,调料瓶乱放、锅具堆叠、台面油污,你找瓶生抽要翻半天——这就是代码里的“坏味道”。于是你决定整理厨房:调料按标签归位,锅具挂上墙,过期食材清掉。这个过程就是“重构”:不改变菜的味道,只让厨房更清爽。本文就用这套厨房比喻,带你理解模式组合的价值与原则、重构的核心与时机、坏味道的识别方法、经典组合实战,以及工程实践中的技术债与节奏。

岗位关联

掌握模式组合与重构,是从“会写代码”迈向“会设计代码”的分水岭,也是架构评审、代码治理与团队协作的底层能力,助你在复杂系统中做出优雅而可持续的决策。

目录
  1. 可视化演示
  2. 模式组合概述
  3. 组合设计原则
  4. 重构核心概念
  5. 坏味道识别
  6. 重构技术方法
  7. 经典组合实战
  8. 工程实践要点
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

模式组合概述

炒一盘鱼香肉丝,要用“滑炒”让肉嫩,用“糖醋”提味,用“勾芡”锁汁——三道技法前前后后配合,才成就一盘经典。设计模式的组合也一样:没有哪个模式是万能钥匙,真正复杂的业务场景,往往是策略、工厂、观察者等多个模式协同作战。

“组合”不是把两个模式随意堆在一起,而是让它们各司其职、通过稳定的接口协作。比如一个支付模块里,工厂负责按类型创建支付方式,策略负责封装每种支付的计算逻辑,观察者负责通知订单状态变化。三者职责清晰,互不越界。

但组合是有代价的——每多引入一个模式,系统就多一层间接性和一套协作规则。模式之间可能产生两两交互,交互数量随着模式数量急剧上升:

就像厨房里新添一口蒸锅很轻松,再添一口高压锅、再添一台烤箱,灶台走位就要重新规划。聪明的厨师懂得“按需添置、间隔留白”,设计者同样要克制地组合模式,把复杂度控制在团队能驾驭的范围。

C(n,2)=n(n−1)2\displaystyle C(n,2) = \frac{n(n-1)}{2}
这个公式告诉我们:3 个模式有 3 对交互,5 个模式就有 10 对交互。模式越多,协作关系的“接线”越密,复杂度呈平方级增长。
  1. 变量 n参与组合的设计模式总数,是公式的唯一输入。
    nn
  2. 两两配对 n(n-1)每个模式都要与其他 n-1 个模式建立协作关系,先数出所有有序配对。
    n×(n−1)n \times (n-1)
  3. 去除重复除以 2AB 与 BA 是同一对协作,必须除以 2 消除顺序带来的重复计数。
    n(n−1)2\frac{n(n-1)}{2}

组合设计原则

主厨的厨房有条铁律:所有食材放在贴标签的冷藏柜里,各灶台只认菜谱不认人——不管谁来掌勺,拿料的位置都一样。这背后是“依赖抽象”的智慧:烧菜的师傅(高层模块)不直接认识送菜的小工(底层模块),而是通过“贴标签的冷藏柜”(抽象接口)取货。

组合设计原则的核心有几点:接口优先,让模式之间只通过抽象协作;组合优于继承,用对象组合代替脆弱的继承树;职责单一,每个模式只解决一类问题。这三个原则共同保证:模式组合里的各个角色像乐队成员各吹各的谱,合起来却是一首完整的曲子。把依赖关系画出来,就是下面这幅图:

电饭煲坏了,换一个同样插口的,炒菜流程完全不受影响——正因为“插口”(接口)是标准化的。组合设计原则的精髓就在于此:让每个模式成为可以热插拔的“标准厨具”,坏哪个换哪个,不影响整条流水线。

H→A←L\displaystyle H \rightarrow A \leftarrow L
依赖倒置原则的浓缩表达:抽象 A 是中间人,高层 H 与底层 L 都向它看齐,彼此不直接认识。
  1. 依赖箭头箭头表示“依赖关系”,H→A 代表高层模块 H 依赖抽象 A,而不关心具体的 L 是谁。
    H→AH \rightarrow A
  2. 双向汇聚底层模块 L 也同样依赖 A,两个实现在抽象接口处汇合。
    L→AL \rightarrow A
  3. 解耦结论H 与 L 之间没有箭头,这正是两者互不直接耦合的数学证明。
    ∄ H→L\nexists\ H \rightarrow L

重构核心概念

主厨每周三都要“大扫除”:炉灶擦亮、刀具归位、冰箱清点。奇怪的是,无论怎么挪动锅碗,晚上端出来的菜,味道和平时一模一样。这正是重构的奥义:改变布局,不动味道。

重构是在不改变外部可观察行为的前提下,对内部结构进行的小步调整。它和“加新功能”是两回事:加功能是换菜单,重构是整理后厨。要想做到“味道不变”,必须有一件事兜底——测试。

重构的两条铁律:一是行为保持,旧代码输入什么输出什么,重构后必须一字不差;二是小步推进,每次只动一小块,改完立刻跑测试。这些铁律可以浓缩成下面的数学表达:

深夜打烊后的大扫除,看不见客人的欢呼,却决定了明早高峰的出菜效率。重构是最安静的投资:表面没有新功能上线,实际是在为下一次变化扫清路障。

∀x∈D, f′(x)=f(x)\displaystyle \forall x \in D,\ f'(x) = f(x)
对输入域 D 中的每一个输入 x,重构后的函数 f' 与原来的函数 f 输出完全一致。这是重构的“行为契约”。
  1. 输入域 D程序所有合法输入组成的集合,相当于“所有的菜谱订单”。
    D={x1,x2,…,xn}D = \{x_1, x_2, \ldots, x_n\}
  2. 原行为 f重构前代码对输入 x 的表现,也就是“老厨房出菜的味道”。
    y=f(x)y = f(x)
  3. 新行为 f'重构后代码的输出函数。等式要求两者对每个输入都相等,才能保证“味道不变”。
    ∀x, f′(x)=f(x)\forall x,\ f'(x) = f(x)

坏味道识别

主厨有套“巡检清单”:灶台有没有积油、冰箱有没有过期食材、调料瓶有没有标签模糊。出现这些迹象,就该停下炒菜,动手收拾。代码世界里也有类似的“气味探测器”——坏味道。

坏味道不是 bug,它是一类“潜在问题信号”:代码重复,同一段逻辑散落在多个地方;长函数,一个方法几百行,读起来像绕迷宫;过度耦合,改 A 就得牵动 B、C、D;发散变化,一个类同时因为多个原因被修改。它们不一定会立刻出错,但一定会让后续维护越来越贵。

识别坏味道可以借助量化指标。其中最经典的是 McCabe 圈复杂度,它统计控制流中独立路径的数量,来衡量一个函数的“岔路”有多乱:

闻到油焦味就关火检查,而不是等浓烟冒起才处理。好厨师会把坏味道扼杀在摇篮里,好工程师同样把 M > 10 的函数拆分成多个小函数,让“厨房”始终保持清爽可巡检。

M=E−N+2P\displaystyle M = E - N + 2P
圈复杂度 M 等于控制流图的边数 E 减去节点数 N,再加 2 倍连通分量数 P。M 越高,函数分支越复杂,越容易藏 bug、越需要重构。
  1. 控制流图把函数画成图,每个语句是一个节点,跳转关系是一条边,相当于把菜谱步骤画成带箭头的流程图。
    N=节点数, E=边数N = \text{节点数},\ E = \text{边数}
  2. 减去 N 加 2P这是图论中计算环路数量的公式,P 是连通分量数(单个普通函数为 1)。
    M=E−N+2PM = E - N + 2P
  3. 经验阈值一般认为 M ≤ 10 属于健康,超过 10 就要考虑拆分了。
    M>10⇒建议重构M > 10 \Rightarrow \text{建议重构}

重构技术方法

大扫除不是乱搬乱放,而是有套路:先擦灶台、再归置调料、最后拖地,顺序错了反而更脏。重构也有自己的“标准动作库”,每一招都对应一个常见的坏味道。

最常用的几招:提取方法,把长函数里的一段独立逻辑抽成命名清晰的小函数;引入接口,让依赖模块面向抽象而非具体类;消除条件,用查找表或多态取代层层 if-else;状态替换,用状态对象取代复杂的布尔标志位。这些招式共同的指向是“单一事实来源”——每段逻辑只在一处存在,改动只动一处:

固定调料的位置,不是为了让这一次做菜更快,而是为了让下次、下下次的每一道菜都更快。重构的标准动作每次只解决一个小问题,但日积月累,代码的“取料路径”会越来越短。

∣Loc(L)∣=1\displaystyle \left| \mathrm{Loc}(L) \right| = 1
逻辑 L 在代码库中的出现位置集合 Loc(L) 的大小必须等于 1。一段业务规则只在一个地方定义,这是消灭重复的根本手段。
  1. 业务逻辑 L一段有名字的业务规则,例如“会员满 99 元免运费”。
    L=运费规则L = \text{运费规则}
  2. 位置集合 Loc(L)所有包含这段逻辑的代码位置的集合,比如定义处、调用处、复制粘贴处。
    Loc(L)={p1,p2,…}\mathrm{Loc}(L) = \{p_1, p_2, \ldots\}
  3. 基数等于 1集合只有一个元素,表示这段规则真正只出现一次,改一处即可全局生效。
    ∣Loc(L)∣=1\left| \mathrm{Loc}(L) \right| = 1

经典组合实战

招牌菜之所以是招牌,在于几种技法的黄金配比。设计模式中最经典的组合也各有“配方”:策略+工厂是替换条件分支的王牌,装饰+组合是动态增强对象能力的标配,MVC 则把展示、控制、数据三块按职责拆分再协作。

以支付系统为例。一个支付模块最初可能是 5 个 if-else 依次判断支付宝、微信、银行卡……每加一种支付方式就要改一段老代码,违背了开闭原则。重构方案:策略模式让每种支付方式成为一个独立类,工厂模式集中负责“按类型创建策略对象”。新增支付方式时,只需注册一个新的策略类,老代码一行不动:

蒸鱼时先大火锁住鲜味再转小火焖透,火候的切换就是“时序组合”。策略+工厂这对黄金搭档,把“选谁干活”和“怎么干活”两件事分开管理,让代码在新增与修改面前保持从容。

T查表=O(1) ≪ Tif-else=O(k)\displaystyle T_{\text{查表}} = O(1) \ \ll \ T_{\text{if-else}} = O(k)
策略表查找的耗时是常数级 O(1),而长长的 if-else 链平均要比较 k/2 次。组合重构不只是“结构变漂亮”,关键路径上还变快了。
  1. k 个分支原有 if-else 链有 k 个条件分支,最坏情况需要比较 k 次。
    最坏情况: k 次比较\text{最坏情况: } k \text{ 次比较}
  2. 线性开销 O(k)分支越多,判断链越长,时间开销随 k 线性增长。
    Tif-else=O(k)T_{\text{if-else}} = O(k)
  3. 表驱动 O(1)工厂的注册表是一个哈希结构,一次查表即可定位到策略,常数时间完成。
    T查表=O(1)T_{\text{查表}} = O(1)

工程实践要点

整理厨房不能只在年底大扫除一次,而要每周固定时间小扫。代码重构同样讲究节奏:不是憋大招重写系统,而是把它嵌入日常开发,随做随清,像刷牙一样自然。

工程实践中,重构要关注几个维度:持续演进,每提交一段代码都比之前更干净一点;重构节奏,坚持小步快跑,避免“大爆炸式重写”;团队协作,约定统一的提交规范,重构与功能变更分开提交方便 review;技术债控制,对“先上线后优化”的债要有台账,设定偿还计划。技术债的代价可以用一个简单模型来体会:

好厨房的秘诀不是从不弄脏,而是“随手清洁”。工程团队的健康度也来自这种日常纪律:每次改动顺手清理一点坏味道,定期偿还技术债,结构就不会在漫长迭代中腐烂。

I(t)=r×t\displaystyle I(t) = r \times t
技术债的累计修理成本 I 等于“利率” r(单位时间新增的修复成本)乘以拖延时间 t。拖得越久,利息越滚越大,最后可能变成“还不起的重写”。
  1. 利率 r代码每多存放一天,因为耦合、重复导致的额外返工成本,相当于“每天新增的擦油污时间”。
    r=单位时间修复成本r = \text{单位时间修复成本}
  2. 时间 t从欠下技术债到偿还之间经过的时间,拖得越长 r 被乘的次数越多。
    t=债务存续时长t = \text{债务存续时长}
  3. 乘积关系利息随拖延时间线性增长——早上擦掉一滴油只需一秒钟,三个月后擦同一块油渍要用钢丝球。
    I=r×tI = r \times t

AI 应用场景

智能客服对话系统的意图路由

阶段:模型推理后处理 / 服务编排

解决的问题:意图识别模型返回几十种意图,传统做法用巨型 if-else 调不同下游服务,每新增一个意图就要改主流程,极易出错还难以回滚。

具体案例:某电商客服机器人用策略+工厂组合重构:每个意图对应一个策略类,工厂按意图名查表创建,新增“退差价”意图只需注册一个新策略类,主路由代码一行不动。

机器学习特征工程管道重构

阶段:数据预处理与特征构建

解决的问题:特征处理代码在多个实验脚本里复制粘贴,改清洗逻辑要同步修改多处,训练与线上推理行为经常不一致。

具体案例:用提取方法与装饰+组合模式,将清洗、归一化、编码拆成可组合的特征处理器,通过配置文件组装管道,训练和线上共用同一套代码。

推荐系统策略 A/B 实验平台

阶段:策略调度与实验评估

解决的问题:每次上线新推荐策略都要改打分主流程,灰度回滚困难,多个实验并行时互相干扰。

具体案例:用策略模式封装不同打分算法,工厂根据实验配置创建对应策略,观察者模式上报实验指标;上线新策略 = 注册一个类并配置流量比例。

Python 演示

python59 行
# -*- coding: utf-8 -*-"""策略模式 + 工厂模式组合,重构支付 if-else 链"""  class PayStrategy:    """支付策略基类,约定所有支付方式实现 pay 方法"""    def pay(self, amount):        raise NotImplementedError  class Alipay(PayStrategy):    def pay(self, amount):        print("支付宝支付: %.2f 元" % amount)  class Wechat(PayStrategy):    def pay(self, amount):        print("微信支付: %.2f 元" % amount)  class Card(PayStrategy):    def pay(self, amount):        print("银行卡支付: %.2f 元" % amount)  class PayFactory:    """策略工厂:用注册表取代 if-else 分发"""    registry = {}     @classmethod    def register(cls, key, strategy):        cls.registry[key] = strategy     @classmethod    def create(cls, key):        if key not in cls.registry:            raise ValueError("不支持的支付方式: %s" % key)        return cls.registry[key]()  # 注册三种支付策略PayFactory.register("alipay", Alipay)PayFactory.register("wechat", Wechat)PayFactory.register("card", Card)  def pay(order, method):    strategy = PayFactory.create(method)    strategy.pay(order["amount"])  if __name__ == "__main__":    orders = [        {"amount": 99.9, "method": "alipay"},        {"amount": 199, "method": "wechat"},        {"amount": 50.5, "method": "card"},    ]    for item in orders:        pay(item, item["method"])
目的:演示如何用“策略 + 工厂”的组合,把支付场景中层层 if-else 重构为可扩展的注册表分发结构。
  1. class PayStrategy:定义策略基类,约定所有支付方式必须实现 pay 方法,这是模式组合的“抽象接口”。
  2. registry = {}用字典充当策略注册表,替代 if-else 的“查询表”。
  3. cls.registry[key] = strategy把策略类登记到注册表,新增支付方式只需一行注册。
  4. if key not in cls.registry:相当于原 if-else 链最后的 else 分支,兜底报错。
  5. return cls.registry[key]()一次查表创建策略对象,时间复杂度 O(1)。
  6. PayFactory.register("alipay", Alipay)注册三种支付策略,把“选择”和“实现”解耦。
  7. strategy.pay(order["amount"])统一调用策略接口,业务方不关心具体是哪个支付类。
  8. if __name__ == "__main__":程序入口,模拟三笔订单走三种支付方式。

常见误区

误区我学了十几种设计模式,把它们全用上才显得高级。一个类里既有工厂、又有观察者、还有装饰器,这样架构肯定很酷。

正解模式是解决问题的厨艺,不是越多越好。像炒菜时把煎、炸、烤、炖全招呼上,食材早废了。组合数公式 C(n,2) = n(n-1)/2 告诉我们,每多一个模式就多几条协作线,复杂度平方级上升。高级感来自“恰到好处”:一个 if-else 用策略就能解决的事,不必硬塞四个模式。

为何常见:初学者刚学会模式后容易“手里拿着锤子看什么都像钉子”,把模式带来的符号价值误当成了设计价值。

误区这代码太烂了,没法重构,直接重写吧,反正功能一样。

正解重构的铁律是行为不变、小步推进;重写是推倒重来,两者天差地别。重写等于开新项目,可能引入新 bug、丢掉隐性业务规则。就像不能说“厨房太乱就拆了重建”,打烊后花两小时归置,比停业装修一个月划算得多。除非代码彻底无法理解且测试缺失,否则优先重构。

为何常见:面对烂代码时本能反应是“眼不见为净”,而且重写比小心翼翼的重构在心理上更有“创造性”的快感。

误区我很熟悉这套代码,不需要测试,直接改就行,出不了事。

正解重构的行为保护伞就是测试。没有测试的重构像不带味觉试菜——你根本不知道哪一步把味道改坏了。哪怕没有现成测试,第一步也应该是先补上核心路径的测试再动手。小步重构里每一步都跑测试,出错时才能立刻定位到最近一步。

为何常见:很多人觉得写测试浪费时间,“我很熟这套代码”的自信最容易放大这种错觉,其实熟悉和正确是两件事。

误区组合模式嘛,就是把对象一层套一层,装饰器包工厂、工厂包观察者,嵌套深度越深越厉害。

正解套娃过度会造成“调试地狱”:一个调用链走五层才摸到真实逻辑,新人看不懂,出问题难排查。组合讲究职责清晰、层次扁平,能用两层解决就不用五层。就像厨房里抽屉套抽屉,找一把勺要开四个柜门,本身就是坏味道。

为何常见:把“抽象层次多”误当成“设计水平高”,忽视了可读性和可调试性才是工程的第一诉求。

面试题

Q1. 下列哪个选项最符合“重构”的定义?

单选题 · easy

  • A.重写所有代码替换新实现
  • B.保持外部行为不变调整结构← 正确答案
  • C.给旧代码增加新的功能特性
  • D.优化数据库表字段索引设计

答案:B。重构是在不改变外部可观察行为的前提下改善内部结构,就像整理厨房不能改变菜的味道。A 是重写,C 是加功能,D 是数据库调优,都不属于重构。

Q2. 模式组合的首要价值是什么?

单选题 · easy

  • A.显著提升系统的代码总行数
  • B.让类的继承层级变得更加复杂
  • C.让多个模式协作解决复杂问题← 正确答案
  • D.用继承来替代所有对象组合

答案:C。模式组合的出发点是让多个模式各司其职、协同解决单一模式搞不定的复杂问题,好比多种厨艺配合出一道招牌菜。行数变多、层级变深都是副作用而非价值。

Q3. 下列哪些代码坏味道适合用策略模式重构?

多选题 · mid

  • A.用条件分支按类型选择不同算法← 正确答案
  • B.多个子类中重复出现相似的初始化
  • C.长函数内部堆满复杂的条件判断逻辑← 正确答案
  • D.一个接口中定义了过多的方法职责

答案:AC。策略模式专门针对“按类型选择算法”和“条件分支堆积”这两类坏味道,把每个分支变成独立策略类。B 适合用模板方法或工厂方法,D 适合用接口隔离原则解决。

Q4. McCabe 圈复杂度公式 M = E - N + 2P 中,P 通常表示什么?

单选题 · mid

  • A.函数参数的个数总和
  • B.控制流图中的连通分量个数← 正确答案
  • C.类的公共方法的数量总数
  • D.递归调用的最大嵌套层数

答案:B。P 是控制流图的连通分量数,普通单个函数等于 1。圈复杂度衡量的是独立路径数量,与参数个数、公共方法数、递归深度没有直接关系。

Q5. 关于“组合优于继承”,下列说法正确的是?

单选题 · mid

  • A.继承在设计模式中永远不该使用
  • B.组合让复用不绑定具体继承树← 正确答案
  • C.组合模式的运行性能永远更高
  • D.所有抽象关系都必须用接口表达

答案:B。组合通过“对象里持有对象”实现复用,不要求双方在同一条继承树上,像可插拔厨具一样灵活。它不代表继承完全不能用,也不保证性能更高,更不强制依赖接口。

Q6. 重构之前必须具备哪些保障?

多选题 · mid

  • A.覆盖核心逻辑的自动化单元测试← 正确答案
  • B.针对行为保持的回归验证工具链← 正确答案
  • C.对输入输出边界情况的完备测试← 正确答案
  • D.对每个变量添加尽量详细的注释

答案:ABC。重构以“行为不变”为铁律,必须靠自动化测试和回归验证来兜底,覆盖输入输出边界才能证明新旧行为一致。写注释跟重构保障无关,过度注释反而可能是坏味道。

Q7. 策略与工厂组合中,工厂的核心作用是什么?

单选题 · hard

  • A.让多个策略类之间建立继承关系
  • B.集中管理策略的创建与选择过程← 正确答案
  • C.提高策略对象每次执行的速度
  • D.彻底消除策略类的公共接口

答案:B。工厂把“创建哪个策略”的决策集中管理,调用方不再关心 if-else 选择逻辑。继承关系由策略类自行设计,性能与接口由策略实现决定,都不是工厂的职责。

Q8. 技术债模型 I(t) = r x t 中,r 代表什么?

单选题 · easy

  • A.项目的代码总行数总量
  • B.每单位时间累积的修复成本← 正确答案
  • C.自动化测试的覆盖率数值
  • D.团队成员代码提交的频繁程度

答案:B。r 是“利率”,即单位时间内因为代码腐化而新增的修复成本。技术债像厨房油渍,拖得越久,单位时间增加的清洁成本越高,与行数、覆盖率、提交频率无关。

Q9. 下列哪些做法符合小步重构的节奏?

多选题 · hard

  • A.每次只提取一个独立的小方法← 正确答案
  • B.每步改动之后立刻运行测试← 正确答案
  • C.边开发新需求边进行大范围重构
  • D.单个重构完成后及时提交代码← 正确答案

答案:ABD。小步重构强调“一次只动一小块、动完就验证、完成后提交”,随时可回退。C 把新需求和大范围重构混在一起,既难测试又难 review,是大忌。

Q10. 模式组合复杂度 C = n(n-1)/2 说明了什么?

单选题 · hard

  • A.组合越多复杂度呈线性增长
  • B.组合数量随模式数平方级增长← 正确答案
  • C.模式数量越多设计必定越好
  • D.应该彻底避免一切模式组合

答案:B。随着模式数 n 增大,两两交互对数按 n 的平方级增长,提醒我们组合要克制。它不否定组合的价值,而是呼吁“按需引入”,更不能因噎废食完全不用。