跳到主要内容

模板方法模式

问题

什么是模板方法模式?它与前端框架的生命周期钩子有什么关系?"好莱坞原则"是什么意思?如何在现代前端开发中应用模板方法模式?

答案

模板方法模式(Template Method Pattern)是 GoF 23 种设计模式之一,属于行为型模式。它在一个方法中定义算法的骨架,将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法的某些步骤。

一句话理解

父类定义"做事的流程",子类填充"每一步怎么做"。React/Vue 的生命周期钩子、Webpack 的 Plugin 钩子、测试框架的 beforeEach/afterEach,本质上都是模板方法模式的体现。


核心概念

UML 结构

四种方法类型

方法类型说明特点
模板方法定义算法骨架,按顺序调用各步骤不可覆盖(final),由父类控制
抽象方法声明但不实现,强制子类实现子类必须实现
钩子方法提供默认实现(通常为空或返回默认值)子类可选覆盖
具体方法父类已实现的通用逻辑子类一般不覆盖
关键区分
  • 抽象方法:子类必须实现,否则编译报错(TypeScript 的 abstract
  • 钩子方法:子类可以选择覆盖,也可以使用默认行为(React 的 componentDidMount 就是钩子——你可以写,也可以不写)

TypeScript 实现

数据报告生成器

以"数据报告生成"为例,流程固定为:获取数据 -> 处理数据 -> 格式化 -> 输出,但每一步的具体实现可以不同。

template-method/report-generator.ts
// 抽象基类 —— 定义算法骨架
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('================');
}
}
template-method/csv-report.ts
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');
}
}
template-method/html-report.ts
// 具体类 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 报告生成完成,已发送邮件通知');
}
}
template-method/usage.ts
// 使用 —— 调用者无需关心内部实现
const csvReport = new CsvReportGenerator('sales-db');
csvReport.generate();

const htmlReport = new HtmlReportGenerator();
htmlReport.generate();
TypeScript 没有 final

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-lifecycle-as-template.ts
// 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-lifecycle-as-template.ts
// 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 注册到特定的钩子点。

webpack-plugin-as-template.ts
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)

测试框架定义了"测试执行流程",开发者通过 beforeEachafterEach 等钩子填充逻辑。

test-framework-as-template.ts
// 测试框架内部的执行流程(伪代码)
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 中间件

中间件管线本质上也是模板方法的思想:框架定义了请求处理的固定流程(接收请求 -> 中间件链 -> 返回响应),开发者填充每个中间件的具体逻辑。

express-middleware-template.ts
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. 页面基类模式

在复杂的中后台系统中,可以用模板方法模式统一页面的初始化流程。

template-method/base-page.ts
// 页面基类 —— 定义通用的页面生命周期
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');
}
}
template-method/user-list-page.ts
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 方式替代

template-method/hooks-alternative.tsx
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>
);
}

高阶函数方式

template-method/hof-alternative.ts
// 用配置对象 + 高阶函数替代继承
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: 模板方法中"抽象方法"和"钩子方法"的区别?

答案

对比抽象方法钩子方法
是否必须实现必须,否则编译报错可选,有默认实现
默认实现有(通常为空或返回默认值)
语义"你必须告诉我怎么做""你可以选择性地干预"
TypeScriptabstract 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: 请举一个在实际项目中使用模板方法模式的例子

答案

以"多渠道消息推送"为例,推送流程固定(校验 -> 格式化 -> 发送 -> 记录日志),但不同渠道(邮件、短信、站内信)的具体实现不同:

notification-system.ts
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',
});

相关链接