TypeScript 设计模式实践:工厂、策略、观察者、组合模式的类型安全实现

用 TypeScript 类型系统重新审视经典设计模式:工厂/策略/观察者/组合模式的类型安全实现,以及函数式风格与 OOP 风格的对照与选型。

设计模式解决的是"变化"的问题:创建方式的变化(工厂)、算法族的变化(策略)、一对多通知的变化(观察者)、树状结构的变化(组合)。TypeScript 的独特之处在于,它的类型系统能把经典 GoF 模式里"靠运行时多态"保证的安全,提前到编译期——模式在 TS 里常常比在 Java/C++ 里更简洁、更安全。

本文用类型安全的方式重新实现四个高频模式,并对照函数式风格——很多模式在 TS 里用"函数 + 数据结构"就能表达得更轻量。


1. 工厂模式(Factory)

1.1 简单工厂 + 判别联合

工厂的核心价值是把"根据条件创建不同对象"的决策集中到一处。TS 的判别联合(Discriminated Union)让工厂的返回类型精确且穷尽:

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

type Circle = Extract<Shape, { kind: 'circle' }>;
type Rectangle = Extract<Shape, { kind: 'rectangle' }>;

function createShape(kind: Shape['kind']): Shape {
  switch (kind) {
    case 'circle': return { kind, radius: 1 };
    case 'rectangle': return { kind, width: 2, height: 3 };
    case 'triangle': return { kind, base: 4, height: 5 };
  }
}

// 关键:没有 default,TypeScript 检查 switch 是否穷尽。
// 如果新增一种 kind,这里会编译报错 —— 穷尽性检查(Exhaustiveness Check)

Shape['kind'] 作为入参、Shape 作为返回类型,工厂的类型契约一目了然;新增类型分支时,编译期强制更新工厂。

1.2 抽象工厂泛型化

当产品本身是一组相关对象时(UI 主题、数据库方言),用泛型工厂把"产品族"抽象出来:

interface Button { render(): string; }
interface Dialog { show(): string; }

interface UIFactory {
  createButton(): Button;
  createDialog(): Dialog;
}

// 产品族 A:浅色主题
class LightThemeFactory implements UIFactory {
  createButton(): Button { return { render: () => '[Light Button]' }; }
  createDialog(): Dialog { return { show: () => 'Light Dialog' }; }
}

// 产品族 B:深色主题
class DarkThemeFactory implements UIFactory {
  createButton(): Button { return { render: () => '[Dark Button]' }; }
  createDialog(): Dialog { return { show: () => 'Dark Dialog' }; }
}

function bootstrap(factory: UIFactory) {
  const button = factory.createButton();
  const dialog = factory.createDialog();
  console.log(button.render(), dialog.show());
}

bootstrap(new DarkThemeFactory());

抽象工厂的 TS 版本用接口 + 具体实现类表达,与 Java 版本几乎一致,区别是接口本身是结构化的,测试时可以传入 mock 对象而不必 implements:

// 结构类型:不必显式 implements
const mockFactory: UIFactory = {
  createButton: () => ({ render: () => '[Mock]' }),
  createDialog: () => ({ show: 'Mock' }),
};

1.3 函数式工厂:工厂即函数

很多"工厂"其实不需要类——一个返回构造函数的函数就是函数式工厂:

interface LoggerOptions {
  level: 'debug' | 'info' | 'warn' | 'error';
  prefix?: string;
}

function createLogger(options: LoggerOptions) {
  const prefix = options.prefix ?? '';
  return {
    debug: (msg: string) => console.debug(`${prefix}[debug] ${msg}`),
    info: (msg: string) => console.info(`${prefix}[info] ${msg}`),
    // ...
  };
}

const logger = createLogger({ level: 'info', prefix: '[API]' });

函数式工厂的优势:无 this 绑定问题、天然闭包私有状态、易组合。大多数"返回某对象"的工厂用函数就够了,抽象工厂只在产品族强关联时才需要类的形式。


2. 策略模式(Strategy)

2.1 接口策略:可插拔算法族

策略模式把"算法"封装成可互换的对象。TS 中一个带签名的接口即可定义策略契约:

interface PricingStrategy {
  calculate(base: number, quantity: number): number;
}

// 正常价
class RegularPricing implements PricingStrategy {
  calculate(base: number, quantity: number) { return base * quantity; }
}

// 会员价(9 折)
class MemberPricing implements PricingStrategy {
  calculate(base: number, quantity: number) { return base * quantity * 0.9; }
}

// 批发价(满 100 件打 8 折)
class WholesalePricing implements PricingStrategy {
  calculate(base: number, quantity: number) {
    return base * quantity * (quantity >= 100 ? 0.8 : 1);
  }
}

class Order {
  private strategy: PricingStrategy;
  constructor(strategy: PricingStrategy) { this.strategy = strategy; }
  setStrategy(strategy: PricingStrategy) { this.strategy = strategy; }
  total(base: number, quantity: number) { return this.strategy.calculate(base, quantity); }
}

const order = new Order(new MemberPricing());
order.total(100, 2); // 180
order.setStrategy(new WholesalePricing()); // 运行期切换算法

2.2 策略注册表:去掉 if-else 链

策略模式最常见的变体是策略注册表——用 Map 代替长串的 if / switch:

type PaymentMethod = 'wechat' | 'alipay' | 'card';

interface PaymentStrategy {
  pay(amount: number): Promise<string>; // 返回支付单号
}

const paymentStrategies: Record<PaymentMethod, PaymentStrategy> = {
  wechat: { pay: (amount) => Promise.resolve(`wx_${amount}`) },
  alipay: { pay: (amount) => Promise.resolve(`ali_${amount}`) },
  card: { pay: (amount) => Promise.resolve(`card_${amount}`) },
};

async function checkout(method: PaymentMethod, amount: number) {
  const strategy = paymentStrategies[method];
  if (!strategy) throw new Error(`不支持的支付方式: ${method}`);
  return strategy.pay(amount);
}

Record<PaymentMethod, PaymentStrategy> 保证每种支付方式都必须提供实现——漏注册一个,编译期就报错。这是"注册表 + 穷尽性"的类型安全红利。

2.3 函数式策略:策略即函数

如果策略只有"一个算法",直接用函数更自然:

type SortStrategy<T> = (items: T[]) => T[];

const byNameAsc: SortStrategy<{ name: string }> = (items) =>
  [...items].sort((a, b) => a.name.localeCompare(b.name));

const byNameDesc: SortStrategy<{ name: string }> = (items) =>
  [...items].sort((a, b) => b.name.localeCompare(a.name));

// 传入策略参数(高阶函数形式,无需类)
function sortItems<T>(items: T[], strategy: SortStrategy<T>): T[] {
  return strategy(items);
}

函数式策略没有"策略对象",只有"策略函数",配合默认参数、partial application 使用起来更轻。一个原则:策略只有一个方法时用函数,多个方法构成协议时用接口。


3. 观察者模式(Observer)

3.1 类型安全的事件发射器

观察者模式在 TS 中最经典的形态是类型安全 EventEmitter——用泛型把"事件名 → 载荷类型"的映射编进类型:

interface Events {
  userLogin: { userId: string; at: Date };
  cartUpdated: { itemCount: number };
  error: { message: string; code: number };
}

type Handler<K extends keyof Events> = (payload: Events[K]) => void;

class TypedEmitter {
  private handlers = new Map<keyof Events, Set<Function>>();

  on<K extends keyof Events>(event: K, handler: Handler<K>): () => void {
    const set = this.handlers.get(event) ?? new Set<Function>();
    set.add(handler);
    this.handlers.set(event, set);
    // 返回取消订阅函数(对称 API)
    return () => this.off(event, handler);
  }

  off<K extends keyof Events>(event: K, handler: Handler<K>): void {
    this.handlers.get(event)?.delete(handler);
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.handlers.get(event)?.forEach((fn) => (fn as Handler<K>)(payload));
  }
}

const bus = new TypedEmitter();
const unsubscribe = bus.on('userLogin', (payload) => {
  // payload 被推断为 { userId: string; at: Date }
  console.log(`用户 ${payload.userId} 登录于 ${payload.at}`);
});

bus.emit('userLogin', { userId: 'u1', at: new Date() }); // 类型安全
// bus.emit('userLogin', { userId: 123 }); // 编译错误:userId 应为 string
unsubscribe(); // 取消订阅

K extends keyof Events + Events[K] 的映射让每个事件名绑定精确的载荷类型,杜绝了手写 EventEmitter 的"any 载荷"地狱。

3.2 可组合的 Subject + Subscription

响应式风格的 Subject(既是观察者又维护订阅列表)在状态管理、WebSocket 推送里很常见:

class Subject<T> {
  private observers = new Set<(value: T) => void>();

  subscribe(observer: (value: T) => void): () => void {
    this.observers.add(observer);
    return () => this.observers.delete(observer);
  }

  next(value: T): void {
    this.observers.forEach((observer) => observer(value));
  }

  /** 从另一个 subject 订阅(管道:A 的值转发到 B) */
  pipe<S>(transform: (value: T) => S): Subject<S> {
    const out = new Subject<S>();
    this.subscribe((v) => out.next(transform(v)));
    return out;
  }
}

// 使用:用户输入防抖管道
const input$ = new Subject<string>();
const length$ = input$.pipe((s) => s.length);
length$.subscribe((len) => console.log(`当前输入长度: ${len}`));
input$.next('hello'); // 当前输入长度: 5

3.3 函数式信号(Signal)风格

前端响应式(React hooks、Vue ref、Preact signals)把观察者模式"反过来":数据持有读取函数,读取即订阅。一个迷你信号实现:

type EffectFn = () => void;

class Signal<T> {
  private value: T;
  private effects = new Set<EffectFn>();

  constructor(initial: T) { this.value = initial; }

  get(): T { return this.value; }

  set(next: T): void {
    if (Object.is(this.value, next)) return;
    this.value = next;
    this.effects.forEach((fn) => fn());
  }

  /** 订阅副作用,返回取消函数 */
  subscribe(fn: EffectFn): () => void {
    this.effects.add(fn);
    return () => this.effects.delete(fn);
  }
}

const count = new Signal(0);
const stop = count.subscribe(() => console.log(`count = ${count.get()}`));
count.set(1); // count = 1
stop();
count.set(2); // 不再输出

函数式风格的观察者:状态即数据,通知即重渲染,比手动订阅/取消更适合 UI 场景。


4. 组合模式(Composite)

4.1 递归树结构类型

组合模式让"单个对象"与"对象集合"用同一接口处理。TS 的递归类型让树结构声明得非常干净:

// 文件系统节点:文件或目录(目录可以包含节点)
interface FSNode {
  name: string;
  size: number;
  // 目录会返回子节点,文件返回空数组
  list(): FSNode[];
}

interface FileNode extends FSNode { type: 'file'; content: string; }
interface DirNode extends FSNode { type: 'dir'; children: FSNode[]; }

function calculateSize(node: FSNode): number {
  if (node.type === 'file') return node.size;
  // 目录:自身不计,递归累加子节点
  return node.children.reduce((sum, child) => sum + calculateSize(child), 0);
}

calculateSize 对文件与目录一视同仁地递归——组合模式的精髓:客户端不需要区分叶子与容器。

4.2 类型安全的 AST:组合模式的高级形态

组合模式最强大的应用是抽象语法树(AST)。TS 的判别联合让 AST 节点的递归类型精确且穷尽:

type Expr =
  | { kind: 'num'; value: number }
  | { kind: 'add'; left: Expr; right: Expr }
  | { kind: 'mul'; left: Expr; right: Expr }
  | { kind: 'var'; name: string };

// 解释器(visitor 风格,配合判别联合穷尽性)
function evaluate(expr: Expr, env: Record<string, number>): number {
  switch (expr.kind) {
    case 'num': return expr.value;
    case 'var': return env[expr.name] ?? 0;
    case 'add': return evaluate(expr.left, env) + evaluate(expr.right, env);
    case 'mul': return evaluate(expr.left, env) * evaluate(expr.right, env);
  }
}

const ast: Expr = {
  kind: 'add',
  left: { kind: 'mul', left: { kind: 'num', value: 3 }, right: { kind: 'var', name: 'x' } },
  right: { kind: 'num', value: 5 },
};

evaluate(ast, { x: 2 }); // 3 * 2 + 5 = 11

新增一种节点(如 sub)时,evaluate 的 switch 会编译报错——编译器强制你补齐所有节点类型的处理。

4.3 函数式组合:flatMap 与 tree map

组合模式的功能版本用递归函数 + 高阶函数表达,例如"映射整棵树":

function mapExpr(expr: Expr, fn: (e: Expr) => Expr): Expr {
  switch (expr.kind) {
    case 'num': return fn(expr);
    case 'var': return fn(expr);
    case 'add': return fn({ kind: 'add', left: mapExpr(expr.left, fn), right: mapExpr(expr.right, fn) });
    case 'mul': return fn({ kind: 'mul', left: mapExpr(expr.left, fn), right: mapExpr(expr.right, fn) });
  }
}

// 对每个数字节点 +1(不可变更新整棵树)
const bumped = mapExpr(ast, (e) =>
  e.kind === 'num' ? { ...e, value: e.value + 1 } : e,
);

不可变 + 纯函数的组合实现,天然适配 React 的 useReducer 状态树与函数式校验器。


5. 函数式 vs OOP:风格对照与选型

5.1 四个模式的风格对照

模式OOP 形态函数式形态函数式的收益
工厂Factory 接口 + 具体工厂类返回对象的普通函数无 this、易组合、闭包私有态
策略Strategy 接口 + 策略类策略函数 + Record 注册表单方法策略直接传函数
观察者Subject 类 + 订阅者对象Signal / 回调集合声明式、与 UI 框架契合
组合树节点接口 + 叶子/容器类判别联合 + 递归函数穷尽性检查、不可变

5.2 选型决策:什么时候用哪种

倾向函数式:

  • 策略/工厂只封装单个算法,无内部状态;
  • 需要不可变更新、易测试(纯函数无 mock);
  • 与 React/Redux/函数式框架共处。

倾向 OOP(类 + 接口):

  • 策略/产品强关联成组(抽象工厂、一个协议多个方法);
  • 需要私有可变状态 + 生命周期(资源管理、连接池);
  • 团队/框架约定(NestJS 的装饰器 + 类依赖注入)。

混合是最常见的生产形态:接口定义契约(OOP),实现用纯函数对象(函数式)。例如 NestJS 的 Injectable 类内部大量使用纯函数工具。

// 混合示例:接口契约 + 函数实现 + 注册表
interface Validator { validate(value: unknown): string[]; }

const validators: Record<string, Validator> = {
  email: {
    validate: (v) => (typeof v === 'string' && v.includes('@') ? [] : ['不是合法邮箱']),
  },
  // ...
};

5.3 模式在 TS 中的"降维"

很多 GoF 模式在 TS 里会自然"消失",因为语言特性已经覆盖了它们:

GoF 模式TS 内建替代说明
迭代器for...of + Symbol.iterator语言级
单例const + 模块作用域模块即单例
外观模块公开 API不必类包装
模板方法高阶函数默认参数回调注入
建造者对象字面量 + satisfies数据驱动

写模式之前先问:语言特性或一个函数是否已经解决了这个问题? 模式是给"变化"兜底的,不是装饰品。


6. 最佳实践

  1. 让类型成为模式的一部分:判别联合 + switch 穷尽性检查,把"忘了处理新分支"变成编译错误。
  2. 策略/工厂优先函数:单方法策略用函数,注册表用 Record<union, impl> 保证穷尽。
  3. 订阅必须可取消:所有 on/subscribe 返回取消函数,防止内存泄漏。
  4. 组合模式优先不可变:树结构用纯函数变换,避免就地修改导致的竞态。
  5. 模式服务于变化点:没有变化点就不要上模式,简单代码永远优于"模式化的复杂"。

将模式与类型系统能力结合,可参考 https://plumephp.com/typescript-type-level-programming/ 中的映射类型与递归工具;在生产后端使用 OOP 容器风格(NestJS 类 + 装饰器),可参考 https://plumephp.com/typescript-decorators-metaprogramming/;在 React/Vue 等声明式框架中落地函数式风格,可参考 https://plumephp.com/typescript-nodejs-backend/ 与前端专题的配套实践。


7. 总结

设计模式在 TypeScript 中获得了"升级":同样的意图,更少的样板,更强的编译期保证。工厂用判别联合获得穷尽性,策略用注册表获得完备性,观察者用泛型事件映射获得载荷安全,组合用递归类型获得结构清晰。

同时要记住模式的本质是应对变化:当变化不存在时,最优雅的实现是"不引入模式"。函数式优先、接口定义契约、类型驱动穷尽——这三条原则能让模式真正成为工程资产而非负担。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. CSS 架构与样式方案:从方法论到现代 CSS 新特性
  2. 可访问性与国际化:WCAG 2.2、ARIA 与 i18n 工程实践
  3. SSR/SSG 渲染模式全景:Next.js App Router、流式渲染与岛屿架构