Skip to content

设计模式总览

设计模式最大的误区,是把它当成 23 个名词背诵表。真实项目里更常见的问题不是"我不知道单例的定义",而是:一个需求改三次以后,原来清爽的函数开始塞满 if-else,对象之间互相 new,测试要启动半个系统,新人一改就牵连一片。

设计模式要解决的不是"代码看起来高级",而是变化来了以后,代码还能有边界地变

一句话先记住:设计模式是变化管理工具,不是类图表演。


一、先看代码怎么坏

假设订单支付一开始只支持支付宝:

ts
class OrderService {
  async pay(orderId: string) {
    const order = await db.findOrder(orderId);
    const result = await alipay.charge(order.amount);
    await db.markPaid(orderId, result.tradeNo);
  }
}

后来加微信,银行卡,企业余额,测试环境假支付,代码很容易变成:

ts
class OrderService {
  async pay(orderId: string, channel: string) {
    const order = await db.findOrder(orderId);

    if (channel === "alipay") {
      // sign, charge, parse callback
    } else if (channel === "wechat") {
      // another sign, charge, parse callback
    } else if (channel === "bank") {
      // another flow
    }

    await db.markPaid(orderId);
  }
}

坏味道不在 if-else 本身,而在变化点混进了稳定流程:

  • 稳定点:查订单,校验金额,发起支付,记录结果
  • 变化点:不同支付渠道的签名,接口,回调格式,错误码

设计模式的工作,就是把变化点挪到合适的位置,让稳定流程保持稳定。


二、模式到底是什么

模式不是框架,不是库,也不是固定代码模板。它更像一组经过反复验证的结构经验:

text
某类变化经常出现
  -> 直接写会让代码耦合
  -> 抽出稳定接口
  -> 把变化封装到独立对象或函数里
  -> 调用方只依赖稳定部分

比如支付渠道可以先抽成接口:

ts
interface PaymentProvider {
  charge(order: Order): Promise<PaymentResult>;
}

class OrderService {
  constructor(private provider: PaymentProvider) {}

  async pay(orderId: string) {
    const order = await db.findOrder(orderId);
    const result = await this.provider.charge(order);
    await db.markPaid(orderId, result.tradeNo);
  }
}

这段代码还没有说出具体模式名,但模式心智已经出现了:

  • 业务流程依赖抽象,不依赖具体渠道
  • 支付渠道可以独立替换
  • 测试时可以注入 fake provider
  • 新增渠道不必改 OrderService

很多设计模式都是这个思想的不同形态。


三、GoF 模式和架构模式不要混在一起

GoF 设计模式通常讲的是对象之间怎么协作:

类型解决的问题例子
创建型对象怎么创建,创建逻辑放哪工厂,建造者,原型,单例
结构型对象怎么组合成更大的结构适配器,装饰器,代理,外观
行为型对象之间怎么通信和分配职责策略,观察者,命令,状态

MVC / MVP / MVVM / 分层 / 六边形架构不是 GoF 模式。它们解决的是更大的问题:页面,用例,领域,数据库,外部系统之间怎么分层和隔离。

不要把它们强行塞进一个分类里。分类错了,判断也会错:

  • 单例讨论的是"一个对象的生命周期和访问方式"
  • MVVM 讨论的是"UI 状态和视图如何同步"
  • 六边形架构讨论的是"业务核心如何不被外部技术绑死"

四、看懂模式的四个问题

学任何模式都可以先问四件事。

1. 它保护哪个变化点?

策略模式保护算法变化,工厂保护创建变化,适配器保护外部接口变化,状态模式保护状态流转变化。说不清变化点,通常就是为了模式而模式。

2. 它把复杂度搬到哪里?

模式不会让复杂度消失,只会搬家。工厂把创建复杂度从业务服务搬到工厂类,观察者把直接调用搬成事件订阅,装饰器把继承层级搬成对象组合。

3. 它让测试更容易还是更难?

好的抽象能替换依赖,隔离副作用。坏的抽象会引入全局状态,生命周期不清,测试必须按顺序跑。

4. 语言或框架有没有更简单的写法?

Java 里常见的接口和类,在 TypeScript 里可能是函数表,在 Go 里可能是小接口,在 Rust 里可能是 trait 或 enum。不要照搬经典 Java 写法。


五、一个最小结构图

text
业务流程(OrderService)
        |
        v
稳定抽象(PaymentProvider)
        |
        +-- AlipayProvider
        +-- WechatProvider
        +-- FakeProvider

这张图比背定义更重要。你要能一眼看出:

  • 上层稳定,下层变化
  • 上层不关心具体实现
  • 新变化从侧面扩展,不是钻进核心流程乱改

六、落地判断

适合引入模式的信号:

  • 同一个 if-else 被复制到多个地方
  • 创建对象需要一堆参数,环境,条件判断
  • 业务逻辑直接依赖第三方 SDK,数据库,HTTP 客户端
  • 状态流转越来越多,靠布尔字段互相牵制
  • 测试必须连接真实外部系统才能跑

先别引入模式的信号:

  • 只有一个实现,短期也看不到第二个
  • 变化方向还没看清,抽象会猜错
  • 团队读不懂这层抽象,维护成本高于收益
  • 用一个函数参数或配置表就能解决

设计模式不是"越早越好",而是"变化足够明确时再切边界"。


七、代价与误用

误用一:把简单逻辑拆成十几个类

如果一段代码只有 20 行,没有复用,没有变化,拆成 StrategyFactoryManager 只会增加阅读成本。

误用二:只看 UML,不看调用方

模式是否成立,要看调用方是否真的变简单。类图漂亮但调用方还要知道所有细节,说明抽象失败。

误用三:拿模式名替代解释

代码评审里说"这里用了工厂模式"没有意义。更好的说法是:"创建渠道依赖环境和租户配置,放在业务服务里会扩散,所以集中到工厂里"。

误用四:忽略语言特性

不是所有模式都需要类。很多行为型模式在 JavaScript / TypeScript 里用函数就够了,在 Go 里用接口和结构体组合就够了。


八、一句话总结

设计模式的核心不是记住名字,而是看清稳定点和变化点,再用最小的结构把变化关在合适的位置。

最后更新: