模板方法模式
问题
什么是模板方法模式?它与前端框架的生命周期钩子有什么关系?"好莱坞原则"是什么意思?如何在现代前端开发中应用模板方法模式?
答案
模板方法模式(Template Method Pattern)是 GoF 23 种设计模式之一,属于行为型模式。它在一个方法中定义算法的骨架,将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法的某些步骤。
父类定义"做事的流程",子类填充"每一步怎么做"。React/Vue 的生命周期钩子、Webpack 的 Plugin 钩子、测试框架的 beforeEach/afterEach,本质上都是模板方法模式的体现。
核心概念
UML 结构
四种方法类型
| 方法类型 | 说明 | 特点 |
|---|---|---|
| 模板方法 | 定义算法骨架,按顺序调用各步骤 | 不可覆盖(final),由父类控制 |
| 抽象方法 | 声明但不实现,强制子类实现 | 子类必须实现 |
| 钩子方法 | 提供默认实现(通常为空或返回默认值) | 子类可选覆盖 |
| 具体方法 | 父类已实现的通用逻辑 | 子类一般不覆盖 |
- 抽象方法:子类必须实现,否则编译报错(TypeScript 的
abstract) - 钩子方法:子类可以选择覆盖,也可以使用默认行为(React 的
componentDidMount就是钩子——你可以写,也可以不写)
TypeScript 实现
数据报告生成器
以"数据报告生成"为例,流程固定为:获取数据 -> 处理数据 -> 格式化 -> 输出,但每一步的具体实现可以不同。
// 抽象基类 —— 定义算法骨架
abstract class ReportGenerator {
// 模板方法:定义不可变的算法流程
// TypeScript 没有 final 关键字,通过约定保证不覆盖
generate(): void {
const rawData = this.fetchData();
const processed = this.processData(rawData);
const formatted = this.formatData(processed);
// 钩子方法:生成前的可选校验
if (this.shouldValidate()) {
this.validate(formatted);
}
this.output(formatted);
// 钩子方法:生成后的可选清理
this.onComplete();
}
// 抽象方法 —— 子类必须实现
protected abstract fetchData(): unknown[];
protected abstract processData(data: unknown[]): Record<string, unknown>;
protected abstract formatData(data: Record<string, unknown>): string;
// 钩子方法 —— 子类可选覆盖
protected shouldValidate(): boolean {
return false;
}
protected validate(data: string): void {
// 默认空实现
}
protected onComplete(): void {
console.log('报告生成完成');
}
// 具体方法 —— 通用逻辑
protected output(content: string): void {
console.log('=== 报告内容 ===');
console.log(content);
console.log('================');
}
}
interface SalesRecord {
product: string;
amount: number;
date: string;
}
// 具体类 A:CSV 报告
class CsvReportGenerator extends ReportGenerator {
private dataSource: string;
constructor(dataSource: string) {
super();
this.dataSource = dataSource;
}
protected fetchData(): SalesRecord[] {
console.log(`从 ${this.dataSource} 获取数据...`);
return [
{ product: 'iPhone', amount: 5999, date: '2026-01-15' },
{ product: 'MacBook', amount: 12999, date: '2026-01-16' },
];
}
protected processData(data: SalesRecord[]): Record<string, unknown> {
const total = data.reduce((sum, item) => sum + item.amount, 0);
return { records: data, total, count: data.length };
}
protected formatData(data: Record<string, unknown>): string {
const records = data.records as SalesRecord[];
const header = 'Product,Amount,Date';
const rows = records.map((r) => `${r.product},${r.amount},${r.date}`);
return [header, ...rows, `Total,,${data.total}`].join('\n');
}
}
// 具体类 B:HTML 报告(覆盖了钩子方法)
class HtmlReportGenerator extends ReportGenerator {
protected fetchData(): SalesRecord[] {
return [
{ product: 'iPad', amount: 3999, date: '2026-02-01' },
];
}
protected processData(data: SalesRecord[]): Record<string, unknown> {
const total = data.reduce((sum, item) => sum + item.amount, 0);
return { records: data, total };
}
protected formatData(data: Record<string, unknown>): string {
const records = data.records as SalesRecord[];
const rows = records
.map((r) => `<tr><td>${r.product}</td><td>${r.amount}</td></tr>`)
.join('');
return `<table><thead><tr><th>Product</th><th>Amount</th></tr></thead><tbody>${rows}</tbody></table>`;
}
// 覆盖钩子方法:启用校验
protected shouldValidate(): boolean {
return true;
}
protected validate(data: string): void {
if (!data.includes('<table>')) {
throw new Error('HTML 格式校验失败');
}
}
protected onComplete(): void {
console.log('HTML 报告生成完成,已发送邮件通知');
}
}
// 使用 —— 调用者无需关心内部实现
const csvReport = new CsvReportGenerator('sales-db');
csvReport.generate();
const htmlReport = new HtmlReportGenerator();
htmlReport.generate();
Java 中模板方法通常用 final 修饰,防止子类覆盖流程。TypeScript 目前不支持 final 关键字,只能通过约定或注释来表达"不要覆盖此方法"的意图。如果需要强约束,可以考虑在运行时检查:
class BaseClass {
constructor() {
if (this.templateMethod !== BaseClass.prototype.templateMethod) {
throw new Error('templateMethod 不允许被覆盖');
}
}
templateMethod(): void {
// 算法骨架
}
}
好莱坞原则
"Don't call us, we'll call you"
模板方法模式体现了好莱坞原则(Hollywood Principle),也叫控制反转(IoC, Inversion of Control):
| 对比维度 | 传统调用 | 好莱坞原则 |
|---|---|---|
| 控制方向 | 子类主动调用父类 | 父类主动调用子类 |
| 流程控制权 | 分散在各子类中 | 集中在父类中 |
| 复用性 | 需要每个子类维护流程 | 流程只写一次 |
| 前端例子 | 手动调用 super.componentDidMount() | React 自动调用 componentDidMount |
- React:你不调用
render(),React 帮你调 - Vue:你不调用
mounted(),Vue 帮你调 - Webpack:你不调用 Plugin 方法,Webpack 在构建流程中自动触发钩子
- Express:你不调用中间件,Express 按洋葱模型依次调用
- Jest:你不调用
beforeEach,Jest 在每个测试前自动执行
前端实际应用
1. React/Vue 生命周期钩子
框架定义了组件从创建到销毁的完整流程,开发者只需"填充"特定的钩子方法。
// React 类组件的生命周期本质上就是模板方法模式
// React 内部的"模板方法"(伪代码)
abstract class ReactComponentLifecycle {
// React 内部控制的流程(模板方法)
mountComponent(): void {
this.constructor(); // 初始化
this.getDerivedStateFromProps(); // 钩子
this.render(); // 抽象方法:必须实现
// DOM 挂载
this.componentDidMount(); // 钩子:可选实现
}
updateComponent(): void {
this.getDerivedStateFromProps(); // 钩子
this.shouldComponentUpdate(); // 钩子:可选,默认 true
this.render(); // 抽象方法
this.getSnapshotBeforeUpdate(); // 钩子
// DOM 更新
this.componentDidUpdate(); // 钩子
}
unmountComponent(): void {
this.componentWillUnmount(); // 钩子
// 清理
}
// 抽象方法 —— 必须实现
abstract render(): JSX.Element;
// 钩子方法 —— 可选覆盖
componentDidMount(): void {}
shouldComponentUpdate(): boolean { return true; }
componentDidUpdate(): void {}
componentWillUnmount(): void {}
}
// Vue 的生命周期同理
// Vue 内部流程(伪代码)
abstract class VueComponentLifecycle {
initComponent(): void {
this.beforeCreate(); // 钩子
// 初始化响应式数据
this.created(); // 钩子
// 编译模板
this.beforeMount(); // 钩子
// 挂载 DOM
this.mounted(); // 钩子
}
updateComponent(): void {
this.beforeUpdate(); // 钩子
// Diff & Patch
this.updated(); // 钩子
}
destroyComponent(): void {
this.beforeUnmount(); // 钩子
// 清理
this.unmounted(); // 钩子
}
// 所有钩子都是可选的(钩子方法)
beforeCreate(): void {}
created(): void {}
beforeMount(): void {}
mounted(): void {}
beforeUpdate(): void {}
updated(): void {}
beforeUnmount(): void {}
unmounted(): void {}
}
更多细节请参考 React 生命周期演变 和 Vue 生命周期。
2. 构建工具 Plugin(Webpack Tapable 钩子)
Webpack 的插件系统本质上是模板方法模式的事件驱动变体:Webpack 定义了完整的构建流程,插件通过 tap 注册到特定的钩子点。
import type { Compiler, Compilation } from 'webpack';
// Webpack 内部的构建流程(简化版模板方法)
class WebpackBuildProcess {
private compiler: Compiler;
build(): void {
// 固定的构建流程 —— 模板方法
this.compiler.hooks.beforeRun.call(this.compiler); // 钩子点
this.compiler.hooks.run.call(this.compiler); // 钩子点
this.compiler.hooks.compile.call(); // 钩子点
// ... 编译过程
this.compiler.hooks.emit.call(); // 钩子点
this.compiler.hooks.done.call(); // 钩子点
}
}
// 开发者的 Plugin —— 填充钩子
class MyWebpackPlugin {
apply(compiler: Compiler): void {
// 注册到特定的钩子点
compiler.hooks.emit.tapAsync(
'MyWebpackPlugin',
(compilation: Compilation, callback: () => void) => {
// 在输出资源之前做自定义操作
console.log('资源即将输出...');
const assets = compilation.getAssets();
console.log(`共 ${assets.length} 个资源`);
callback();
}
);
compiler.hooks.done.tap('MyWebpackPlugin', (stats) => {
console.log(`构建完成,耗时 ${stats.endTime! - stats.startTime!}ms`);
});
}
}
3. 测试框架(Jest/Vitest)
测试框架定义了"测试执行流程",开发者通过 beforeEach、afterEach 等钩子填充逻辑。
// 测试框架内部的执行流程(伪代码)
class TestRunner {
// 模板方法:固定的测试执行流程
async runSuite(suite: TestSuite): Promise<void> {
await this.runHooks(suite.beforeAll); // 钩子
for (const test of suite.tests) {
await this.runHooks(suite.beforeEach); // 钩子
await this.runTest(test); // 执行测试
await this.runHooks(suite.afterEach); // 钩子
}
await this.runHooks(suite.afterAll); // 钩子
}
}
// 开发者使用 —— 填充钩子
describe('UserService', () => {
let db: Database;
// 钩子方法:在每个测试前初始化
beforeEach(async () => {
db = await Database.connect();
await db.seed();
});
// 钩子方法:在每个测试后清理
afterEach(async () => {
await db.cleanup();
await db.disconnect();
});
it('should create user', async () => {
const user = await UserService.create({ name: 'Alice' });
expect(user.id).toBeDefined();
});
});
4. Express/Koa 中间件
中间件管线本质上也是模板方法的思想:框架定义了请求处理的固定流程(接收请求 -> 中间件链 -> 返回响应),开发者填充每个中间件的具体逻辑。
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// 框架定义的流程:请求 -> 中间件1 -> 中间件2 -> ... -> 路由处理 -> 响应
// 开发者填充每个"步骤"
// 步骤1:日志中间件
app.use((req: Request, _res: Response, next: NextFunction) => {
console.log(`${req.method} ${req.url}`);
next(); // 交还控制权给框架
});
// 步骤2:认证中间件
app.use((req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization;
if (!token) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
next();
});
// 步骤3:路由处理
app.get('/api/users', (_req: Request, res: Response) => {
res.json({ users: [] });
});
5. 页面基类模式
在复杂的中后台系统中,可以用模板方法模式统一页面的初始化流程。
// 页面基类 —— 定义通用的页面生命周期
abstract class BasePage<TData = unknown> {
protected data: TData | null = null;
protected loading = false;
// 模板方法:页面初始化流程
async init(): Promise<void> {
try {
this.loading = true;
this.showLoading();
await this.checkPermission(); // 步骤1:权限校验
this.data = await this.fetchData(); // 步骤2:获取数据
this.render(); // 步骤3:渲染页面
this.bindEvents(); // 步骤4:绑定事件
this.onReady(); // 钩子:页面就绪
} catch (error) {
this.onError(error as Error); // 钩子:错误处理
} finally {
this.loading = false;
this.hideLoading();
}
}
// 模板方法:页面销毁流程
destroy(): void {
this.unbindEvents();
this.onDestroy();
this.data = null;
}
// 抽象方法 —— 子类必须实现
protected abstract fetchData(): Promise<TData>;
protected abstract render(): void;
protected abstract bindEvents(): void;
// 钩子方法 —— 子类可选覆盖
protected async checkPermission(): Promise<void> {
// 默认不做权限校验
}
protected unbindEvents(): void {}
protected onReady(): void {}
protected onDestroy(): void {}
protected onError(error: Error): void {
console.error('页面初始化失败:', error.message);
}
// 具体方法 —— 通用逻辑
private showLoading(): void {
document.getElementById('loading')?.classList.add('visible');
}
private hideLoading(): void {
document.getElementById('loading')?.classList.remove('visible');
}
}
interface User {
id: number;
name: string;
role: string;
}
// 具体页面:用户列表页
class UserListPage extends BasePage<User[]> {
private container: HTMLElement;
constructor(container: HTMLElement) {
super();
this.container = container;
}
// 必须实现的抽象方法
protected async fetchData(): Promise<User[]> {
const response = await fetch('/api/users');
return response.json();
}
protected render(): void {
if (!this.data) return;
this.container.innerHTML = this.data
.map((user) => `<div class="user-card">${user.name} - ${user.role}</div>`)
.join('');
}
protected bindEvents(): void {
this.container.addEventListener('click', this.handleClick);
}
// 覆盖钩子方法
protected async checkPermission(): Promise<void> {
const hasPermission = await fetch('/api/check-permission?page=user-list');
if (!(await hasPermission.json()).allowed) {
throw new Error('无权访问用户列表');
}
}
protected unbindEvents(): void {
this.container.removeEventListener('click', this.handleClick);
}
protected onReady(): void {
console.log(`用户列表加载完成,共 ${this.data?.length} 条数据`);
}
private handleClick = (e: Event): void => {
const target = e.target as HTMLElement;
if (target.classList.contains('user-card')) {
console.log('点击了用户卡片');
}
};
}
// 使用
const container = document.getElementById('app')!;
const page = new UserListPage(container);
page.init();
模板方法 vs 策略模式
这两个模式经常被放在一起比较。核心区别在于:模板方法用继承实现,策略模式用组合实现。
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 实现方式 | 继承(abstract class) | 组合(接口 + 注入) |
| 控制权 | 父类控制流程 | 调用者控制选择 |
| 变化粒度 | 改变算法的某些步骤 | 替换整个算法 |
| 关系 | is-a(子类是一种父类) | has-a(持有一个策略) |
| 扩展方式 | 新增子类 | 新增策略对象 |
| 流程复用 | 流程在父类中复用 | 无流程复用,策略独立 |
| 耦合度 | 子类与父类紧耦合 | 策略与上下文松耦合 |
| 前端典型 | React 生命周期、Webpack Plugin | 表单验证策略、排序策略 |
// 模板方法:继承,改变部分步骤
abstract class DataExporter {
export(data: unknown[]): void {
const validated = this.validate(data);
const formatted = this.format(validated);
this.save(formatted);
}
protected abstract format(data: unknown[]): string;
protected validate(data: unknown[]): unknown[] { return data; }
protected save(content: string): void { console.log(content); }
}
class JsonExporter extends DataExporter {
protected format(data: unknown[]): string {
return JSON.stringify(data, null, 2);
}
}
// 策略模式:组合,替换整个算法
type FormatStrategy = (data: unknown[]) => string;
const formatStrategies: Record<string, FormatStrategy> = {
json: (data) => JSON.stringify(data, null, 2),
csv: (data) => data.map((row) => Object.values(row as object).join(',')).join('\n'),
};
function exportData(data: unknown[], format: string): string {
const strategy = formatStrategies[format];
return strategy(data);
}
更多关于策略模式的内容请参考 策略模式。
用组合替代继承
在现代前端开发中,继承已经不太受推崇(React Hooks、Vue Composition API 都在远离继承)。模板方法模式的思想可以用组合 + 高阶函数来实现,更加灵活。
Hooks 方式替代
import { useState, useEffect, useCallback } from 'react';
// 用 Hook 实现"模板方法"的流程
function usePageLifecycle<TData>(options: {
fetchData: () => Promise<TData>;
checkPermission?: () => Promise<boolean>;
onReady?: (data: TData) => void;
onError?: (error: Error) => void;
}) {
const [data, setData] = useState<TData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const init = useCallback(async () => {
try {
setLoading(true);
// 步骤1:权限校验(可选)
if (options.checkPermission) {
const hasPermission = await options.checkPermission();
if (!hasPermission) throw new Error('无权限');
}
// 步骤2:获取数据
const result = await options.fetchData();
setData(result);
// 步骤3:就绪回调(可选钩子)
options.onReady?.(result);
} catch (err) {
const error = err as Error;
setError(error);
options.onError?.(error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
init();
}, [init]);
return { data, loading, error, reload: init };
}
// 使用 —— 无需继承,通过参数"填充步骤"
function UserListPage() {
const { data, loading, error } = usePageLifecycle({
fetchData: async () => {
const res = await fetch('/api/users');
return res.json() as Promise<User[]>;
},
checkPermission: async () => {
const res = await fetch('/api/check-permission');
return (await res.json()).allowed;
},
onReady: (users) => {
console.log(`加载了 ${users.length} 个用户`);
},
});
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
高阶函数方式
// 用配置对象 + 高阶函数替代继承
interface PipelineStep<TContext> {
name: string;
execute: (ctx: TContext) => Promise<TContext> | TContext;
optional?: boolean;
}
// 流程引擎 —— 模板方法的函数式版本
function createPipeline<TContext>(steps: PipelineStep<TContext>[]) {
return async (initialContext: TContext): Promise<TContext> => {
let ctx = initialContext;
for (const step of steps) {
try {
console.log(`执行步骤: ${step.name}`);
ctx = await step.execute(ctx);
} catch (error) {
if (!step.optional) throw error;
console.warn(`可选步骤 ${step.name} 失败,跳过`);
}
}
return ctx;
};
}
// 定义具体流程
interface ReportContext {
rawData: unknown[];
processedData: Record<string, unknown> | null;
output: string;
}
const generateReport = createPipeline<ReportContext>([
{
name: '获取数据',
execute: async (ctx) => {
const data = await fetch('/api/data').then((r) => r.json());
return { ...ctx, rawData: data };
},
},
{
name: '数据校验',
optional: true, // 可选步骤,等同于"钩子方法"
execute: (ctx) => {
if (ctx.rawData.length === 0) throw new Error('数据为空');
return ctx;
},
},
{
name: '数据处理',
execute: (ctx) => {
return { ...ctx, processedData: { items: ctx.rawData, count: ctx.rawData.length } };
},
},
{
name: '格式化输出',
execute: (ctx) => {
return { ...ctx, output: JSON.stringify(ctx.processedData, null, 2) };
},
},
]);
// 使用
const result = await generateReport({
rawData: [],
processedData: null,
output: '',
});
- 用继承(传统模板方法):流程固定且复杂、步骤之间有大量共享状态、面向对象风格的代码库
- 用组合(Hooks/高阶函数):流程需要灵活配置、React/Vue 函数式组件、追求低耦合和可测试性
现代前端趋势是优先使用组合,但理解模板方法模式的思想依然重要,因为你每天使用的框架(React、Vue、Webpack、Jest)底层都在用这个模式。
常见面试问题
Q1: 什么是模板方法模式?它解决了什么问题?
答案:
模板方法模式在父类中定义算法的骨架(即执行步骤的顺序),将具体步骤的实现延迟到子类。它解决的核心问题是代码复用和流程统一:
- 多个子类有相同的执行流程,但某些步骤的实现不同
- 希望流程只定义一次,避免各子类各自维护导致不一致
- 需要在固定流程中预留扩展点(钩子)
abstract class OrderProcessor {
// 模板方法:固定流程
process(order: Order): void {
this.validate(order); // 步骤1:校验
this.calculatePrice(order); // 步骤2:计算价格(抽象)
this.applyDiscount(order); // 步骤3:折扣(钩子,可选)
this.submit(order); // 步骤4:提交
}
protected abstract calculatePrice(order: Order): void;
protected applyDiscount(order: Order): void {} // 钩子
private validate(order: Order): void { /* 通用校验 */ }
private submit(order: Order): void { /* 通用提交 */ }
}
Q2: 模板方法中"抽象方法"和"钩子方法"的区别?
答案:
| 对比 | 抽象方法 | 钩子方法 |
|---|---|---|
| 是否必须实现 | 必须,否则编译报错 | 可选,有默认实现 |
| 默认实现 | 无 | 有(通常为空或返回默认值) |
| 语义 | "你必须告诉我怎么做" | "你可以选择性地干预" |
| TypeScript | abstract method() | 普通方法,提供空实现 |
| React 类比 | render()(必须实现) | componentDidMount()(可选) |
| Vue 类比 | setup()(函数式组件必须) | mounted()(可选) |
abstract class Component {
// 抽象方法 —— 不实现会报错
abstract render(): string;
// 钩子方法 —— 有默认实现,子类可选覆盖
shouldUpdate(): boolean {
return true;
}
onMounted(): void {
// 默认空实现
}
}
Q3: 好莱坞原则是什么?在前端哪里体现?
答案:
好莱坞原则("Don't call us, we'll call you")是指高层组件调用底层组件,而不是反过来。在模板方法中,父类(高层)在固定流程中调用子类(底层)的方法,子类不需要主动调用父类的流程。
前端中的体现:
// 1. React —— 你不调用 render,React 帮你调
class App extends React.Component {
render() { return <div>Hello</div>; } // React 框架在合适时机调用
}
// 2. Vue —— 你不调用 mounted,Vue 帮你调
export default {
mounted() { console.log('挂载完成'); } // Vue 框架自动调用
};
// 3. Webpack Plugin —— 你不调用处理函数,Webpack 帮你调
class MyPlugin {
apply(compiler: Compiler) {
compiler.hooks.done.tap('MyPlugin', (stats) => {
// Webpack 在构建完成时自动调用
});
}
}
// 4. Jest —— 你不调用 beforeEach,Jest 帮你调
beforeEach(() => {
// Jest 在每个测试前自动调用
});
Q4: 模板方法模式和策略模式的区别?什么时候用哪个?
答案:
核心区别是继承 vs 组合、改变部分步骤 vs 替换整个算法:
// 模板方法:改变算法的"某些步骤"
// 适用场景:流程固定,只是某些步骤不同
abstract class Authenticator {
login(credentials: Credentials): void {
this.validate(credentials); // 通用
this.authenticate(credentials); // 不同平台不同实现
this.onSuccess(); // 通用
}
protected abstract authenticate(credentials: Credentials): void;
}
class OAuthAuthenticator extends Authenticator {
protected authenticate(credentials: Credentials): void {
// OAuth 认证
}
}
// 策略模式:替换"整个算法"
// 适用场景:算法之间完全独立,运行时可切换
interface SortStrategy<T> {
sort(data: T[]): T[];
}
class QuickSort<T> implements SortStrategy<T> {
sort(data: T[]): T[] { /* 快排 */ return data; }
}
class MergeSort<T> implements SortStrategy<T> {
sort(data: T[]): T[] { /* 归并 */ return data; }
}
// 运行时切换策略
const sorter = new DataSorter(new QuickSort());
sorter.setStrategy(new MergeSort()); // 动态切换
选择建议:
- 有固定流程 + 部分步骤可变 -> 模板方法
- 算法完全独立 + 需要运行时切换 -> 策略模式
Q5: 前端框架的生命周期是如何体现模板方法模式的?
答案:
以 React 为例,框架定义了组件从创建到销毁的完整流程(算法骨架),开发者通过覆盖特定的生命周期方法来"填充"自定义逻辑:
整个流程由 React Fiber 调度器控制,开发者无法改变执行顺序,只能在预留的钩子点填充逻辑,这就是典型的模板方法模式。
更多内容请参考 React 生命周期演变 和 Vue 生命周期。
Q6: TypeScript 如何防止子类覆盖模板方法?
答案:
TypeScript 目前没有 final 关键字(Java 中用 final 修饰模板方法防止覆盖),但有几种替代方案:
// 方案1:运行时检查(最实用)
abstract class BaseProcessor {
constructor() {
const proto = Object.getPrototypeOf(this);
if (proto.process !== BaseProcessor.prototype.process) {
throw new Error('process() 是模板方法,不允许覆盖');
}
}
process(): void {
this.step1();
this.step2();
}
protected abstract step1(): void;
protected abstract step2(): void;
}
// 方案2:使用 private + 公开入口(推荐)
abstract class SafeProcessor {
// 私有方法无法被覆盖
#execute(): void {
this.step1();
this.step2();
}
// 公开入口调用私有模板方法
run(): void {
this.#execute();
}
protected abstract step1(): void;
protected abstract step2(): void;
}
// 方案3:使用 @sealed 装饰器(需配置 experimentalDecorators)
function sealed(
_target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
descriptor.writable = false;
descriptor.configurable = false;
return descriptor;
}
Q7: 如何用组合(Hooks)替代继承实现模板方法模式?
答案:
现代前端提倡"组合优于继承"。传统模板方法用 abstract class 实现,现在可以用 Hooks + 配置对象达到相同效果:
// 传统继承方式
abstract class DataFetcher<T> {
async load(): Promise<void> {
this.showLoading();
const data = await this.fetch();
this.transform(data);
this.hideLoading();
}
protected abstract fetch(): Promise<T>;
protected transform(data: T): void {}
private showLoading(): void {}
private hideLoading(): void {}
}
// 组合方式 —— React Hook
function useDataFetcher<T, R = T>(config: {
fetch: () => Promise<T>;
transform?: (data: T) => R;
onSuccess?: (data: R) => void;
onError?: (error: Error) => void;
}) {
const [data, setData] = useState<R | null>(null);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const raw = await config.fetch();
const result = config.transform ? config.transform(raw) : (raw as unknown as R);
setData(result);
config.onSuccess?.(result);
} catch (err) {
config.onError?.(err as Error);
} finally {
setLoading(false);
}
}, []);
return { data, loading, load };
}
// 使用 —— 无需继承
function UserPage() {
const { data, loading } = useDataFetcher({
fetch: () => fetch('/api/users').then((r) => r.json()),
transform: (users: User[]) => users.filter((u) => u.active),
onSuccess: (users) => console.log(`加载了 ${users.length} 个活跃用户`),
});
// ...渲染
}
| 对比 | 继承方式 | 组合方式(Hooks) |
|---|---|---|
| 扩展性 | 单继承限制 | 可自由组合多个 Hook |
| 可测试性 | 需要 mock 父类 | 可直接 mock 函数 |
| 类型安全 | 依赖 abstract | 依赖泛型参数 |
| 代码量 | 较多(类 + 子类) | 较少(函数 + 配置) |
| 适用场景 | OOP 代码库 | React/Vue 函数式组件 |
Q8: Webpack 的 Tapable 钩子机制和模板方法模式的关系?
答案:
Webpack 的 Tapable 是模板方法模式的事件驱动变体。传统模板方法用继承实现,Tapable 用发布订阅实现,但核心思想一致:框架定义固定流程,开发者在预留的钩子点注入逻辑。
import { SyncHook, AsyncSeriesHook } from 'tapable';
// Webpack Compiler 内部(简化)
class Compiler {
hooks = {
// 定义钩子点(等同于模板方法中的"钩子方法")
beforeCompile: new SyncHook<[]>(),
compile: new SyncHook<[CompilationParams]>(),
emit: new AsyncSeriesHook<[Compilation]>(),
done: new SyncHook<[Stats]>(),
};
// 模板方法:固定的构建流程
run(): void {
this.hooks.beforeCompile.call();
const params = this.newCompilationParams();
this.hooks.compile.call(params);
// ... 编译
this.hooks.emit.callAsync(compilation, () => {
this.hooks.done.call(stats);
});
}
}
// 开发者的 Plugin —— 通过 tap 注册钩子(等同于子类覆盖方法)
class BundleAnalyzerPlugin {
apply(compiler: Compiler): void {
compiler.hooks.done.tap('BundleAnalyzerPlugin', (stats) => {
// 在构建完成后分析 bundle
analyzeBundles(stats);
});
}
}
| 传统模板方法 | Tapable 变体 |
|---|---|
abstract method() | new SyncHook() |
| 子类覆盖方法 | hooks.xxx.tap() |
| 只能一个子类实现 | 多个 Plugin 可注册同一钩子 |
| 编译时绑定 | 运行时动态注册 |
Q9: 模板方法模式有什么缺点?如何避免?
答案:
| 缺点 | 说明 | 解决方案 |
|---|---|---|
| 继承耦合 | 子类依赖父类实现细节 | 用组合替代继承(Hook/高阶函数) |
| 扩展受限 | Java/TypeScript 单继承 | 用接口 + Mixin 组合 |
| 理解成本 | 需要理解父类的完整流程 | 良好的文档和注释 |
| 违反里氏替换 | 子类可能破坏父类行为 | 使用 final(Java)或运行时检查 |
| 步骤数爆炸 | 步骤太多时父类臃肿 | 适时拆分,引入策略模式 |
// ❌ 反面案例:步骤太多,父类臃肿
abstract class GodPage {
init(): void {
this.step1(); this.step2(); this.step3();
this.step4(); this.step5(); this.step6();
this.step7(); this.step8(); this.step9();
// ... 10+ 步骤
}
}
// ✅ 改进:将相关步骤分组,用组合替代
function createPagePipeline(config: {
auth: AuthConfig;
data: DataConfig;
render: RenderConfig;
}) {
return async () => {
await authPipeline(config.auth); // 认证流程
const data = await dataPipeline(config.data); // 数据流程
await renderPipeline(config.render, data); // 渲染流程
};
}
Q10: 请举一个在实际项目中使用模板方法模式的例子
答案:
以"多渠道消息推送"为例,推送流程固定(校验 -> 格式化 -> 发送 -> 记录日志),但不同渠道(邮件、短信、站内信)的具体实现不同:
interface NotificationPayload {
userId: string;
title: string;
content: string;
priority: 'low' | 'medium' | 'high';
}
abstract class NotificationSender {
// 模板方法:固定的推送流程
async send(payload: NotificationPayload): Promise<boolean> {
// 步骤1:校验
if (!this.validate(payload)) {
console.error('校验失败');
return false;
}
// 步骤2:格式化(不同渠道格式不同)
const formatted = this.format(payload);
// 步骤3:限流检查(钩子,可选覆盖)
if (this.shouldRateLimit(payload)) {
console.warn('触发限流,消息已暂缓');
return false;
}
// 步骤4:发送(不同渠道发送方式不同)
const success = await this.deliver(formatted);
// 步骤5:记录日志
this.log(payload, success);
return success;
}
// 抽象方法
protected abstract format(payload: NotificationPayload): string;
protected abstract deliver(content: string): Promise<boolean>;
// 钩子方法
protected shouldRateLimit(_payload: NotificationPayload): boolean {
return false;
}
// 具体方法
private validate(payload: NotificationPayload): boolean {
return !!(payload.userId && payload.title && payload.content);
}
private log(payload: NotificationPayload, success: boolean): void {
console.log(`[${this.getChannel()}] ${payload.title} -> ${success ? '成功' : '失败'}`);
}
protected abstract getChannel(): string;
}
// 邮件渠道
class EmailSender extends NotificationSender {
protected format(payload: NotificationPayload): string {
return `<h1>${payload.title}</h1><p>${payload.content}</p>`;
}
protected async deliver(content: string): Promise<boolean> {
// 调用邮件服务 API
await fetch('/api/email/send', { method: 'POST', body: content });
return true;
}
// 邮件渠道启用限流
protected shouldRateLimit(payload: NotificationPayload): boolean {
return payload.priority === 'low';
}
protected getChannel(): string { return 'Email'; }
}
// 短信渠道
class SmsSender extends NotificationSender {
protected format(payload: NotificationPayload): string {
return `【通知】${payload.title}: ${payload.content.slice(0, 70)}`;
}
protected async deliver(content: string): Promise<boolean> {
await fetch('/api/sms/send', { method: 'POST', body: content });
return true;
}
protected getChannel(): string { return 'SMS'; }
}
// 使用
const emailSender = new EmailSender();
await emailSender.send({
userId: 'u001',
title: '订单发货通知',
content: '您的订单已发货,预计3天内送达',
priority: 'medium',
});