跳到主要内容
预计阅读 25 分钟

设计模式的哲学 —— 何时用/何时不用、过度设计识别、现代语言特性对模式的影响

掌握了 23 种设计模式、学了 SOLID 原则、认识了反模式之后,最重要的问题来了:什么时候该用?什么时候不该用? 这一章不教你新的模式,而是教你”判断力”——知道何时出手、何时收手,这才是从”会用”到”精通”的最后一步。

📋 开篇自测:你已经知道多少?

  1. 你能说出一个”不用任何设计模式反而更好”的场景吗?
  2. “YAGNI”原则是什么意思?它和设计模式的关系是什么?
  3. TypeScript 的联合类型 + 类型收窄能替代哪些设计模式?

一、何时该用设计模式

1.1 三个信号告诉你该用模式了

信号一:重复出现的条件分支

当你发现自己在多个地方写类似的 if-else 或 switch-case 来区分不同类型的行为,这通常暗示需要策略模式或工厂模式。

// 代码中出现了三次以上这种结构?该考虑策略模式了
if (paymentMethod === 'wechat') {
  // 微信支付逻辑
} else if (paymentMethod === 'alipay') {
  // 支付宝逻辑
} else if (paymentMethod === 'credit') {
  // 信用卡逻辑
}

信号二:修改一处影响多处

当你改了一个类,发现要连带修改五六个其他文件,说明耦合度太高。适配器、外观、中介者可以帮助解耦。

信号三:难以测试

当你写单元测试时发现不得不 mock 大量依赖、或者根本无法隔离测试某个模块,通常是因为违反了依赖倒置原则。引入接口 + 依赖注入可以改善。

1.2 “Rule of Three”原则

有一条朴素但实用的经验法则:一段相似的代码出现三次以上,才考虑抽象。

第一次写,直接写。 第二次遇到类似需求,可以容忍一些重复。 第三次出现时,你已经有足够的样本来判断该如何抽象——是用策略模式、模板方法,还是简单的函数提取。

过早抽象的危险在于:你对问题域的理解还不够深,抽出来的抽象可能是错的。错误的抽象比重复的代码更难维护。

1.3 分层决策框架

当你面对一个设计决策时,可以按照这个从简单到复杂的顺序思考:

1. 能用函数解决吗?           → 用函数
2. 需要多态行为吗?           → 用接口 + 策略/工厂
3. 需要控制创建过程吗?       → 用创建型模式
4. 需要组合/转换对象结构吗?  → 用结构型模式
5. 需要协调对象间的通信吗?   → 用行为型模式
6. 需要架构级别的组织吗?     → 用架构模式

每一层都应该是前一层不够用时才升级。永远从最简单的方案开始。

🤔 想一想 回想一下你的项目经历,有没有一个”幸好当初用了设计模式”的案例?又有没有一个”当初用了模式后来发现是多余的”案例?


二、何时不该用设计模式

2.1 YAGNI:你不需要它

YAGNI(You Ain’t Gonna Need It)是极限编程(XP)的核心原则之一。它说的是:不要为了”将来可能需要”的功能提前设计。

// 反例:目前只需要邮件通知,但"以后可能要加短信、微信",
// 所以提前搞了一套完整的策略模式 + 工厂模式 + 注册表...

// 策略接口
interface NotificationStrategy { send(msg: string): void; }
// 抽象工厂
abstract class NotificationFactory { abstract create(): NotificationStrategy; }
// 具体工厂
class EmailNotificationFactory extends NotificationFactory { /*...*/ }
// 注册表
class NotificationRegistry { /*...*/ }
// ... 写了 150 行,只为了发一封邮件

// 正例:先用最简单的方式实现
async function sendNotification(userId: string, message: string): Promise<void> {
  const user = await getUser(userId);
  await sendEmail(user.email, message);
}
// 等真的需要加短信时,再重构。到那时你会更清楚需求到底是什么样的。

2.2 简单问题不需要复杂方案

并不是所有条件分支都需要策略模式。如果一个 if-else 只有两三个分支,且这些分支的逻辑很简短(不超过 5 行),直接写就好。

// 这个完全没必要用策略模式
function getGreeting(hour: number): string {
  if (hour < 12) return '早上好';
  if (hour < 18) return '下午好';
  return '晚上好';
}

// 硬套策略模式就是过度设计
interface GreetingStrategy { greet(): string; }
class MorningGreeting implements GreetingStrategy { greet() { return '早上好'; } }
class AfternoonGreeting implements GreetingStrategy { greet() { return '下午好'; } }
class EveningGreeting implements GreetingStrategy { greet() { return '晚上好'; } }
class GreetingContext {
  // ... 50 行代码,就为了返回三个字符串?
}

2.3 原型验证阶段不需要模式

当你还在验证一个产品想法是否可行时(MVP 阶段),代码的首要目标是快速验证假设。这个阶段写出完美的代码是一种浪费——如果这个产品想法被证伪了,这些代码都会被丢掉。

好的开发节奏是:

  1. 原型阶段:快速、简单、可工作。别管代码质量。
  2. 验证成功,准备长期维护:花时间重构,引入合适的模式。
  3. 持续迭代:随着需求变化,逐步演进设计。

三、过度设计的识别与应对

3.1 过度设计的六大征兆

征兆一:代码行数远超需求复杂度

一个只需要 50 行代码就能实现的功能,你写了 300 行——多出来的 250 行都是”为了未来扩展”的抽象层。

征兆二:存在只有一个实现的接口

// 这个接口只有一个实现,而且看不到第二个实现的可能性
interface UserRepository {
  findById(id: string): Promise<User>;
}

class PostgresUserRepository implements UserRepository {
  findById(id: string): Promise<User> { /* ... */ }
}

// 除非你真的计划支持多种数据库,否则这个接口是多余的

注意:为了测试 Mock 而定义的接口是合理的。但如果你的项目从不写单元测试,这个接口就只是增加了间接层。

征兆三:为了三级深度的”将来可能”而增加复杂度

“将来可能要支持插件” → “插件可能需要生命周期钩子” → “钩子可能需要优先级排序”……停下来。先做第一层就够了。

征兆四:继承层次超过三层

Animal → Mammal → Pet → Dog → Labrador → YellowLabrador

当继承超过三层,几乎总是设计有问题。优先考虑组合(Composition)而不是继承(Inheritance)。

征兆五:一个类的构造函数需要超过 5 个参数

这通常意味着这个类承担了太多职责。先审视能否拆分,而不是急着上建造者模式。

征兆六:团队成员无法理解你的代码

如果一个有经验的同事需要花 30 分钟才能理解你的代码结构,那大概率是过度设计了。好的设计应该让人看一眼就理解意图。

3.2 应对过度设计的心法

心法一:延迟决策

不确定用什么模式?那就不用。先写直白的代码,等痛点真正出现再重构。重构比预测便宜。

心法二:用数据说话

不要靠直觉判断”这段代码将来会变”。看实际的变更历史——哪些文件改动频繁?哪些逻辑真的被扩展过?用 git log 做决策比凭空猜测靠谱得多。

心法三:Strangler Fig 模式

如果你已经陷入了过度设计,不要试图一次性推倒重来。像”绞杀榕”一样,新代码逐步替换旧代码,一次替换一小块,每次替换都是一次简化的机会。


四、现代语言特性对设计模式的影响

GoF 的 23 种模式诞生于 1994 年的 C++ 时代。30 年过去了,现代语言(特别是 TypeScript/JavaScript)的特性让很多模式的实现方式发生了巨大变化,有些模式甚至变得”多余”了。

4.1 函数是一等公民 → 简化策略和命令

在 C++/Java 中,函数不能独立存在,必须包在类里面。所以策略模式需要定义一个接口 + 多个实现类。但在 TypeScript 中,函数本身就是值:

// Java 风格的策略模式
interface SortStrategy { sort(data: number[]): number[]; }
class BubbleSortStrategy implements SortStrategy { sort(data: number[]) { /*...*/ } }
class QuickSortStrategy implements SortStrategy { sort(data: number[]) { /*...*/ } }

// TypeScript 中,一个函数就够了
type SortFn = (data: number[]) => number[];
const bubbleSort: SortFn = (data) => { /* ... */ return data; };
const quickSort: SortFn = (data) => { /* ... */ return data; };

function processData(data: number[], sortFn: SortFn): number[] {
  return sortFn(data);
}

同理,命令模式在很多场景下可以用闭包代替:

// 传统命令模式:定义接口 + 类
interface Command { execute(): void; undo(): void; }

// 如果不需要 undo,一个函数就是一个命令
type SimpleCommand = () => void;
const commandQueue: SimpleCommand[] = [];
commandQueue.push(() => console.log('执行操作 A'));
commandQueue.push(() => console.log('执行操作 B'));
commandQueue.forEach(cmd => cmd());

4.2 联合类型 + 类型收窄 → 简化访问者和状态

TypeScript 的标签联合(Discriminated Union)可以优雅地替代访问者模式:

// 访问者模式的 TypeScript 替代方案
type ASTNode =
  | { type: 'function'; name: string; lineCount: number }
  | { type: 'variable'; name: string; kind: 'const' | 'let' }
  | { type: 'import'; source: string };

// 不需要 Visitor 接口,直接用 switch + 类型收窄
function analyzeNode(node: ASTNode): string {
  switch (node.type) {
    case 'function':
      return `函数 ${node.name},${node.lineCount} 行`; // TypeScript 知道这里有 lineCount
    case 'variable':
      return `变量 ${node.name},声明方式: ${node.kind}`; // 知道这里有 kind
    case 'import':
      return `导入 ${node.source}`; // 知道这里有 source
  }
}

优势:编译时的穷尽检查(exhaustiveness check)。如果你新增了一种节点类型但忘了在 switch 中处理,TypeScript 会报错。

局限:当节点类型分布在多个包中,或者需要频繁增加新操作时,传统的访问者模式仍然有优势。

4.3 装饰器语法 → 简化装饰器模式

TypeScript 的装饰器语法让装饰器模式变得极其简洁。下面的示例使用 legacy experimental decorators(需要 experimentalDecorators: true),这也是 Angular、NestJS 等框架存量代码使用的语法。新项目建议优先使用 TC39 Stage 3 装饰器(无需开启该选项):

// 装饰器工厂:缓存方法返回值(legacy experimental decorators)
function Cacheable(ttlMs: number = 5000) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    // 注意:cache 定义在装饰器工厂内部,是方法级别的——
    // 同一个类的所有实例会共享同一份缓存(因为它们共享同一个方法描述符)。
    // 如果需要实例级别的独立缓存,可以用 WeakMap<object, Map> 以 this 为 key。
    const cache = new Map<string, { value: any; expiry: number }>();

    descriptor.value = async function (...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);

      if (cached && Date.now() < cached.expiry) {
        return cached.value;
      }

      const result = await originalMethod.apply(this, args);
      cache.set(key, { value: result, expiry: Date.now() + ttlMs });
      return result;
    };

    return descriptor;
  };
}

// 装饰器工厂:重试
function Retryable(maxRetries: number = 3) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      for (let i = 0; i < maxRetries; i++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (err) {
          if (i === maxRetries - 1) throw err;
          await new Promise(r => setTimeout(r, (i + 1) * 1000));
        }
      }
    };

    return descriptor;
  };
}

// 使用:一行注解搞定
class ProductAPI {
  @Cacheable(10000)
  @Retryable(3)
  async getProduct(id: string): Promise<Product> {
    const response = await fetch(`/api/products/${id}`);
    return response.json();
  }
}

4.4 Async/Await → 简化观察者和迭代器

async/await + AsyncIterator 让异步数据流的处理变得极其自然,很多时候不需要显式的观察者模式:

// 使用 AsyncGenerator 代替显式的观察者模式
async function* watchFileChanges(dir: string): AsyncIterableIterator<FileChangeEvent> {
  // 内部实现用事件监听,但对外暴露的是简洁的迭代器接口
  const watcher = createWatcher(dir);
  try {
    while (true) {
      const event = await watcher.nextEvent();
      yield event;
    }
  } finally {
    watcher.close();
  }
}

// 使用者:简洁的 for-await-of
for await (const change of watchFileChanges('./src')) {
  console.log(`文件变更: ${change.path}`);
  if (change.type === 'modified') {
    await recompile(change.path);
  }
}

4.5 模式并没有消失,只是变了形态

下面这张对照表总结了现代 TypeScript 对经典模式的影响:

经典模式现代 TypeScript 替代/简化方案模式是否仍有价值
策略函数参数、Map<string, Function>复杂策略仍需类
命令闭包、函数队列需要撤销时仍需类
观察者EventEmitter、RxJS、AsyncIterator仍然核心,形式变了
迭代器Symbol.iterator / Symbol.asyncIterator语言原生支持
装饰器@decorator 语法语法糖化,本质不变
访问者标签联合 + switch简单场景可替代
状态标签联合 + reducer简单场景可替代
单例ES Module 导出实例简化到一行

关键结论:模式背后的思想不会过时,但实现方式应该与时俱进。 用 2026 年的语言特性去实现 1994 年的代码结构,是一种对技术的浪费。

🤔 想一想 随着 AI 辅助编程的普及(如 GitHub Copilot、Claude),设计模式的学习是更重要了还是更不重要了?当 AI 可以自动生成代码时,“设计判断力”和”代码质量直觉”的价值是否反而提升了?


五、本章小结

这一章没有教新模式,而是教了更重要的东西——判断力

核心要点:

  1. Rule of Three:重复三次再抽象,避免过早设计。
  2. YAGNI:不要为”将来可能”的需求提前设计。
  3. 六大征兆帮你识别过度设计:代码膨胀、单实现接口、深层继承、参数爆炸、三级假设、同事看不懂。
  4. 现代语言特性(函数式、联合类型、装饰器语法、async/await)让很多经典模式的实现大幅简化,但模式背后的思想依然有效。
  5. 从简单开始,按需演进,这是最重要的设计哲学。

十章正文到此全部结束。接下来的结束篇,让我们站在更高处回望整段旅程——从 23 种模式中提炼出几条可以带走一辈子的设计原则,以及从”会用模式”到”形成设计直觉”的进阶方向。


📝 结尾自测

  1. “Rule of Three”原则的含义是什么?它如何帮助你避免过早抽象?
  2. 列举过度设计的三个征兆,并各举一个例子。
  3. TypeScript 的联合类型可以在哪些场景下替代传统的访问者模式?其局限性是什么?
  4. 函数是一等公民这个特性如何影响策略模式和命令模式的实现?
  5. 你认为设计模式在 AI 辅助编程时代的价值是增加了还是减少了?请给出你的论据。

购买课程解锁全部内容

写出优雅代码:10 章掌握 23 种设计模式

¥29.90