设计模式解决的是"变化"的问题:创建方式的变化(工厂)、算法族的变化(策略)、一对多通知的变化(观察者)、树状结构的变化(组合)。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. 最佳实践
- 让类型成为模式的一部分:判别联合 +
switch穷尽性检查,把"忘了处理新分支"变成编译错误。 - 策略/工厂优先函数:单方法策略用函数,注册表用
Record<union, impl>保证穷尽。 - 订阅必须可取消:所有
on/subscribe返回取消函数,防止内存泄漏。 - 组合模式优先不可变:树结构用纯函数变换,避免就地修改导致的竞态。
- 模式服务于变化点:没有变化点就不要上模式,简单代码永远优于"模式化的复杂"。
将模式与类型系统能力结合,可参考 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 中获得了"升级":同样的意图,更少的样板,更强的编译期保证。工厂用判别联合获得穷尽性,策略用注册表获得完备性,观察者用泛型事件映射获得载荷安全,组合用递归类型获得结构清晰。
同时要记住模式的本质是应对变化:当变化不存在时,最优雅的实现是"不引入模式"。函数式优先、接口定义契约、类型驱动穷尽——这三条原则能让模式真正成为工程资产而非负担。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。