设计模式不是万能钥匙

面试里被问「你熟悉哪些设计模式」,很多人能一口气背出二十三个名字;但真正写代码时,又常常在「要不要上工厂」和「直接创建不就完了」之间反复横跳。问题在于,设计模式被当成了目标,而不是手段。

设计模式本质是已被验证过的、应对变化经验的总结:它描述的是「一类问题」与「一类解法」之间的对应关系。只有当你的代码里真的出现了那类问题——创建逻辑开始膨胀、算法需要频繁替换、对象职责纠缠不清——模式才有用武之地。

模式是给「已经出现的变化」准备的,不是给「也许会出现的变化」准备的。为了预防想象中的需求而引入模式,往往是在制造复杂度。

本文按创建型、结构型、行为型三条线,各挑最有代表性的模式讲清它到底解决什么问题;再补上坏味道识别与安全重构手法,最后聊聊什么时候该对模式说「不」。

本文核心知识点
  • 设计模式描述问题与解法之间的对应关系,是手段而非目标。
  • 创建型工厂、单例、构建者,解决「对象怎么来」的问题。
  • 结构型适配器、装饰器、代理,解决「对象怎么组合」的问题。
  • 行为型策略、观察者、责任链,解决「职责怎么分配与协作」的问题。
  • 重构在不改变外部行为的前提下改善内部结构,需要测试作为安全网。
设计模式示意:相互嵌合的拼装积木、齿轮与重组曲线箭头组成的蓝色扁平插画
模式像可替换的积木:真正有价值的不是积木本身,而是它能随需求变化被重新组合。

创建型模式:工厂、单例与构建者

创建型模式关注对象的诞生过程。当创建代码出现在越来越多的分支里,它就会从细节演变成负担。

# 问题:创建逻辑散落在调用方,新增类型要改多处
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)

观察者模式用于「一对多」的通知:当一个对象状态变化时,自动通知所有订阅者,这是事件驱动与发布订阅的底层思想。责任链模式把请求沿一条链传递,每个节点决定处理还是转发,适合审批流、过滤器与中间件。

代码坏味道识别

重构通常从一个信号开始——代码里出现了「坏味道」。识别坏味道是决定要不要动手的第一步。

坏味道表现常见对策
过长函数一个函数几百行,靠注释分段提炼函数、以查询取代临时变量
重复代码同样的逻辑散落在多处提炼函数、上移方法
发散式变化一个类因多种原因被修改按职责拆分类
霰弹式修改一处改动要动十几个文件内聚相关逻辑,减少耦合
过长参数列表函数参数超过四五个引入参数对象、构建者
依恋情结方法频繁访问别的类的数据搬移方法与字段

值得注意的是,「坏味道」只是提示,不是判决。发现过长函数不等于必须拆——如果一个函数虽然长但逻辑是线性的、只做一件事,拆开反而增加了跳转成本。重构也需要判断力。