设计模式不是万能钥匙
面试里被问「你熟悉哪些设计模式」,很多人能一口气背出二十三个名字;但真正写代码时,又常常在「要不要上工厂」和「直接创建不就完了」之间反复横跳。问题在于,设计模式被当成了目标,而不是手段。
设计模式本质是已被验证过的、应对变化经验的总结:它描述的是「一类问题」与「一类解法」之间的对应关系。只有当你的代码里真的出现了那类问题——创建逻辑开始膨胀、算法需要频繁替换、对象职责纠缠不清——模式才有用武之地。
模式是给「已经出现的变化」准备的,不是给「也许会出现的变化」准备的。为了预防想象中的需求而引入模式,往往是在制造复杂度。
本文按创建型、结构型、行为型三条线,各挑最有代表性的模式讲清它到底解决什么问题;再补上坏味道识别与安全重构手法,最后聊聊什么时候该对模式说「不」。
- 设计模式描述问题与解法之间的对应关系,是手段而非目标。
- 创建型工厂、单例、构建者,解决「对象怎么来」的问题。
- 结构型适配器、装饰器、代理,解决「对象怎么组合」的问题。
- 行为型策略、观察者、责任链,解决「职责怎么分配与协作」的问题。
- 重构在不改变外部行为的前提下改善内部结构,需要测试作为安全网。
创建型模式:工厂、单例与构建者
创建型模式关注对象的诞生过程。当创建代码出现在越来越多的分支里,它就会从细节演变成负担。
# 问题:创建逻辑散落在调用方,新增类型要改多处
if kind == "wechat":
pay = WechatPay(app_id, mch_id, key)
elif kind == "alipay":
pay = AlipayPay(app_id, private_key)
else:
raise ValueError("unsupported")
# 工厂:把创建逻辑收敛到一处,调用方只依赖接口
class PaymentFactory:
_registry = {}
@classmethod
def register(cls, kind, creator):
cls._registry[kind] = creator
@classmethod
def create(cls, kind, **kwargs) -> Payment:
if kind not in cls._registry:
raise ValueError(f"unsupported: {kind}")
return cls._registry[kind](**kwargs)
PaymentFactory.register("wechat", WechatPay)
PaymentFactory.register("alipay", AlipayPay)
pay = PaymentFactory.create("wechat", app_id="wx1001", mch_id="1900001", key="k_9f2c")
构建者模式解决的是「构造函数参数太多」的问题:当一个对象有十几个可选参数时,用链式调用或分步构建,比一个超长参数列表可读得多。单例模式则要慎用——它本质是一个全局变量,会掩盖依赖关系、增加测试难度,很多场景下用依赖注入反而更好。
结构型模式:适配器、装饰器与代理
结构型模式关注对象之间的组合方式,核心思想是通过组合而非继承来扩展行为。
- 适配器:把一个不兼容的接口转换成客户端期望的接口,常用于接入第三方 SDK。
- 装饰器:在不修改原类的前提下,层层叠加额外职责,例如给请求加上缓存、重试与日志。
- 代理:为对象提供一个替身,控制访问、延迟加载或增加权限校验。
三者的区别常被混淆。一句话概括:适配器改变接口,装饰器增强功能,代理控制访问。装饰器与代理在代码结构上可能很像,但意图完全不同——前者是对调用方透明的能力叠加,后者是把调用方与真实对象隔开。想清楚「意图」,才不会在相似结构里选错工具。
行为型模式:策略、观察者与责任链
行为型模式关注对象之间如何协作、职责如何分配。以「策略」为例,它把一组可互换的算法封装起来,让调用方在运行时选择,从而消除大段条件分支。
# 问题:用 if/elif 写运费计算,新增规则就要改这个函数
def shipping_fee(rule, weight):
if rule == "standard":
return 8 + weight * 1.0
elif rule == "express":
return 15 + weight * 2.0
elif rule == "free":
return 0
raise ValueError("unknown rule")
# 策略:把每条规则封装成独立对象,新增规则不改旧代码
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
@abstractmethod
def fee(self, weight: float) -> float:
raise NotImplementedError
class Standard(ShippingStrategy):
def fee(self, weight): return 8 + weight * 1.0
class Express(ShippingStrategy):
def fee(self, weight): return 15 + weight * 2.0
STRATEGIES = {"standard": Standard(), "express": Express()}
def shipping_fee(rule, weight):
return STRATEGIES[rule].fee(weight)
观察者模式用于「一对多」的通知:当一个对象状态变化时,自动通知所有订阅者,这是事件驱动与发布订阅的底层思想。责任链模式把请求沿一条链传递,每个节点决定处理还是转发,适合审批流、过滤器与中间件。
代码坏味道识别
重构通常从一个信号开始——代码里出现了「坏味道」。识别坏味道是决定要不要动手的第一步。
| 坏味道 | 表现 | 常见对策 |
|---|---|---|
| 过长函数 | 一个函数几百行,靠注释分段 | 提炼函数、以查询取代临时变量 |
| 重复代码 | 同样的逻辑散落在多处 | 提炼函数、上移方法 |
| 发散式变化 | 一个类因多种原因被修改 | 按职责拆分类 |
| 霰弹式修改 | 一处改动要动十几个文件 | 内聚相关逻辑,减少耦合 |
| 过长参数列表 | 函数参数超过四五个 | 引入参数对象、构建者 |
| 依恋情结 | 方法频繁访问别的类的数据 | 搬移方法与字段 |
值得注意的是,「坏味道」只是提示,不是判决。发现过长函数不等于必须拆——如果一个函数虽然长但逻辑是线性的、只做一件事,拆开反而增加了跳转成本。重构也需要判断力。
重构手法与安全网
重构的定义有一条关键前提:不改变外部可观察的行为。所以重构的第一步永远是建立安全网——有了测试,你才敢大动手脚。
# 重构步骤示例:把「计算价格」从混杂逻辑中提炼出来
# 1) 先用现有行为写一个特征测试(characterization test)锚定现状
def test_total_price():
assert total_price(cart) == 128.0
# 2) 提炼函数:把小段逻辑抽出并命名,一次只做一步
def total_price(cart):
return sum(item_price(i) for i in cart.items)
def item_price(item):
base = item.unit_price * item.qty
return base * (1 - item.discount) if item.discount else base
# 3) 每改一步就运行测试,绿了再继续;红了立刻回退
# 4) 最后检查重复代码,用「以查询取代临时变量」进一步消除中间变量
常用重构手法可以归为几类:提炼(提炼函数、提炼类)、搬移(搬移函数、搬移字段)、简化条件(分解条件表达式、以卫语句取代嵌套条件),以及重组数据(以对象取代数据值)。手法不必记全,关键是「小步、可回退、每步都跑测试」。
何时不该用模式
模式用错地方,比不用模式更糟。以下场景要格外克制:
- 只有一个实现:为唯一的类造一个接口,往往是过度设计。
- 结构还没稳定:需求仍在剧烈变化时,先让代码跑起来,等形态清晰再抽象。
- 团队不熟悉:引入团队没人懂的模式,维护成本会超过收益。
- 模式成了炫技:如果模式的唯一作用是让代码「看起来高级」,那就该删掉它。
判断标准很朴素:引入模式之后,新增一个同类需求的改动量是否明显变小了?如果答案是否定的,那这个模式就是在增加负担。模式的价值始终由「应对变化的成本」来衡量,而不是由模式本身的名气来衡量。