您现在的位置是:网站首页 > 模板方法模式(Template Method)的算法骨架定义文章详情
模板方法模式(Template Method)的算法骨架定义
陈川
【
JavaScript
】
55719人已围观
12028字
模板方法模式(Template Method)的算法骨架定义
模板方法模式是一种行为设计模式,它在父类中定义了一个算法的框架,允许子类在不改变算法结构的情况下重写特定步骤。这种模式的核心在于将不变的部分封装在父类,可变的部分通过子类实现。
模式结构与角色
模板方法模式包含两个主要角色:
- 抽象类(Abstract Class):定义算法骨架和模板方法,包含基本操作的抽象声明和具体实现
- 具体类(Concrete Class):实现抽象类中定义的抽象操作,完成算法中与特定子类相关的步骤
// 抽象类
class AbstractClass {
// 模板方法,定义算法骨架
templateMethod() {
this.primitiveOperation1();
this.primitiveOperation2();
this.concreteOperation();
this.hook();
}
// 基本操作,子类必须实现
primitiveOperation1() {
throw new Error("必须实现primitiveOperation1方法");
}
primitiveOperation2() {
throw new Error("必须实现primitiveOperation2方法");
}
// 具体操作,已有实现
concreteOperation() {
console.log("抽象类中的具体操作");
}
// 钩子方法,子类可选择覆盖
hook() {}
}
// 具体类
class ConcreteClass extends AbstractClass {
primitiveOperation1() {
console.log("具体类A实现的operation1");
}
primitiveOperation2() {
console.log("具体类A实现的operation2");
}
}
JavaScript实现特点
在JavaScript中实现模板方法模式有其特殊性:
- JavaScript没有抽象类的原生支持,需要通过抛出错误模拟抽象方法
- 原型继承机制使得模式实现更加灵活
- 可以使用高阶函数替代类继承实现类似效果
// 使用高阶函数实现模板方法
function createBeverage(brew, addCondiments) {
return {
prepareRecipe() {
this.boilWater();
brew();
this.pourInCup();
addCondiments && addCondiments();
},
boilWater() {
console.log("把水煮沸");
},
pourInCup() {
console.log("倒入杯子");
}
};
}
// 创建具体饮料
const coffee = createBeverage(
() => console.log("用沸水冲泡咖啡"),
() => console.log("加糖和牛奶")
);
实际应用场景
模板方法模式在前端开发中有广泛的应用场景:
表单验证流程
class FormValidator {
validate(formData) {
this.beforeValidation();
const errors = this.doValidate(formData);
this.afterValidation();
return errors;
}
beforeValidation() {
console.log("验证前准备...");
}
// 抽象方法,子类实现具体验证逻辑
doValidate() {
throw new Error("必须实现doValidate方法");
}
afterValidation() {
console.log("验证后处理...");
}
}
class LoginFormValidator extends FormValidator {
doValidate(formData) {
const errors = [];
if (!formData.username) {
errors.push("用户名不能为空");
}
if (!formData.password) {
errors.push("密码不能为空");
}
return errors;
}
}
组件生命周期管理
React类组件的生命周期方法就是模板方法模式的典型应用:
class ReactComponent {
componentDidMount() {}
shouldComponentUpdate() {}
componentDidUpdate() {}
componentWillUnmount() {}
// 模板方法
render() {
this.beforeRender();
const content = this.doRender();
this.afterRender();
return content;
}
beforeRender() {}
doRender() {
throw new Error("必须实现doRender方法");
}
afterRender() {}
}
钩子方法的使用技巧
钩子方法是一种可选覆盖的方法,它为模板方法提供额外的扩展点:
class DataProcessor {
process(data) {
this.beforeProcessing(data);
const result = this.doProcessing(data);
this.afterProcessing(result);
return result;
}
beforeProcessing(data) {
// 钩子方法,默认空实现
}
doProcessing(data) {
throw new Error("必须实现doProcessing方法");
}
afterProcessing(result) {
// 钩子方法,默认空实现
}
}
class CSVProcessor extends DataProcessor {
beforeProcessing(data) {
console.log("开始处理CSV数据");
}
doProcessing(data) {
return data.split("\n").map(row => row.split(","));
}
}
模式优势与适用场景
模板方法模式特别适用于以下场景:
- 多个类包含几乎相同的算法,只有某些细微差别
- 需要控制子类扩展点,确保核心算法不被修改
- 存在固定的处理流程,但具体步骤实现可能不同
// 支付流程示例
class PaymentProcessor {
processPayment(amount) {
this.validatePayment(amount);
const paymentResult = this.executePayment(amount);
this.notifyUser(paymentResult);
return paymentResult;
}
validatePayment(amount) {
if (amount <= 0) throw new Error("金额必须大于0");
}
executePayment() {
throw new Error("必须实现executePayment方法");
}
notifyUser(result) {
console.log(`支付结果: ${result.status}`);
}
}
class CreditCardPayment extends PaymentProcessor {
executePayment(amount) {
console.log(`通过信用卡支付${amount}元`);
return { status: "success", amount };
}
}
class AlipayPayment extends PaymentProcessor {
executePayment(amount) {
console.log(`通过支付宝支付${amount}元`);
return { status: "success", amount };
}
}
与策略模式的比较
模板方法模式和策略模式都用于封装算法,但有以下区别:
- 模板方法使用继承,策略模式使用组合
- 模板方法定义算法骨架,策略模式定义完整算法
- 模板方法在编译时确定结构,策略模式在运行时切换
// 策略模式实现对比
const paymentStrategies = {
creditCard: amount => ({ status: "success", amount }),
alipay: amount => ({ status: "success", amount })
};
function processPayment(strategy, amount) {
if (amount <= 0) throw new Error("金额必须大于0");
const result = strategy(amount);
console.log(`支付结果: ${result.status}`);
return result;
}
JavaScript特有的实现变体
在JavaScript中,可以利用闭包和工厂函数实现更灵活的模板方法:
function createDataExporter(formatData, afterExport = () => {}) {
return {
export(data) {
console.log("准备导出数据...");
const formatted = this.validate(data) && formatData(data);
this.writeToFile(formatted);
afterExport();
console.log("导出完成");
},
validate(data) {
return Array.isArray(data) && data.length > 0;
},
writeToFile(data) {
console.log(`写入文件: ${JSON.stringify(data)}`);
}
};
}
const jsonExporter = createDataExporter(
data => JSON.stringify(data, null, 2),
() => console.log("JSON导出后处理")
);
处理异步操作
现代前端开发中经常需要处理异步操作,模板方法模式也可以适应:
class AsyncDataLoader {
async load() {
try {
await this.beforeLoad();
const data = await this.fetchData();
const processed = await this.processData(data);
await this.afterLoad(processed);
return processed;
} catch (error) {
await this.handleError(error);
throw error;
}
}
async beforeLoad() {}
async fetchData() {
throw new Error("必须实现fetchData方法");
}
async processData(data) {
return data;
}
async afterLoad(data) {}
async handleError(error) {
console.error("加载错误:", error.message);
}
}
class UserLoader extends AsyncDataLoader {
async fetchData() {
const response = await fetch("/api/users");
return response.json();
}
async processData(users) {
return users.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`
}));
}
}
浏览器环境中的实际案例
浏览器API封装是模板方法模式的常见应用:
class AnalyticsTracker {
track(event, data) {
if (!this.shouldTrack(event)) return;
const formattedData = this.formatData(data);
this.sendToAnalytics(formattedData);
this.logLocal(event, formattedData);
}
shouldTrack(event) {
return true; // 默认跟踪所有事件
}
formatData(data) {
return {
...data,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
}
sendToAnalytics(data) {
throw new Error("必须实现sendToAnalytics方法");
}
logLocal(event, data) {
console.log(`[Analytics] ${event}`, data);
}
}
class GA4Tracker extends AnalyticsTracker {
sendToAnalytics(data) {
// 实际实现会使用gtag等
console.log("发送数据到GA4:", data);
}
shouldTrack(event) {
return !event.startsWith("debug_");
}
}
测试中的模板方法
模板方法模式可以简化测试代码的编写:
class TestCase {
run() {
this.setUp();
try {
this.test();
console.log("测试通过");
} catch (error) {
console.error("测试失败:", error.message);
} finally {
this.tearDown();
}
}
setUp() {}
test() {
throw new Error("必须实现test方法");
}
tearDown() {}
}
class UserTest extends TestCase {
setUp() {
this.user = new User("John");
}
test() {
if (this.user.name !== "John") {
throw new Error("用户名不正确");
}
}
tearDown() {
this.user = null;
}
}
与函数式编程的结合
在函数式编程范式下,模板方法可以通过高阶函数实现:
function withLogging(wrappedFunction) {
return function(...args) {
console.log(`开始执行: ${wrappedFunction.name}`);
try {
const result = wrappedFunction.apply(this, args);
console.log("执行成功");
return result;
} catch (error) {
console.error("执行失败:", error.message);
throw error;
}
};
}
// 使用示例
const fetchWithLogging = withLogging(async function fetchData(url) {
const response = await fetch(url);
return response.json();
});
前端框架中的应用模式
现代前端框架中的许多概念都体现了模板方法模式的思想:
// Vue组件示例
const FormMixin = {
methods: {
submit() {
if (!this.validate()) return;
this.beforeSubmit();
this.submitForm()
.then(() => this.onSuccess())
.catch(error => this.onError(error));
},
validate() {
throw new Error("必须实现validate方法");
},
beforeSubmit() {},
submitForm() {
throw new Error("必须实现submitForm方法");
},
onSuccess() {
this.$emit("success");
},
onError(error) {
console.error("提交错误:", error);
}
}
};
// 使用mixin
Vue.component("UserForm", {
mixins: [FormMixin],
methods: {
validate() {
return this.username && this.password;
},
submitForm() {
return axios.post("/api/users", {
username: this.username,
password: this.password
});
}
}
});
性能优化考虑
在使用模板方法模式时,需要注意以下性能问题:
- 避免在模板方法中执行不必要的操作
- 考虑将钩子方法的空实现改为null检查
- 对于性能关键路径,可以内联部分操作
class OptimizedProcessor {
process(data) {
// 内联简单操作而不是调用方法
if (data == null) throw new Error("数据不能为空");
const start = performance.now();
const result = this.doProcess(data);
const duration = performance.now() - start;
// 只有需要时才调用钩子
this.onProcessed && this.onProcessed(result, duration);
return result;
}
doProcess(data) {
throw new Error("必须实现doProcess方法");
}
}
设计原则体现
模板方法模式体现了多个面向对象设计原则:
- 好莱坞原则:"不要调用我们,我们会调用你" - 子类不直接调用父类
- 开闭原则 - 对扩展开放,对修改关闭
- 单一职责原则 - 每个类只关注自己的特殊逻辑
class ReportGenerator {
generate() {
const data = this.fetchData();
const processed = this.processData(data);
const formatted = this.formatReport(processed);
return this.outputReport(formatted);
}
fetchData() {
throw new Error("必须实现fetchData方法");
}
processData(data) {
// 默认不处理
return data;
}
formatReport(data) {
throw new Error("必须实现formatReport方法");
}
outputReport(report) {
console.log(report);
return report;
}
}
复杂流程管理
对于复杂流程,模板方法可以帮助维护清晰的执行顺序:
class OrderFulfillment {
async fulfill(order) {
await this.validateOrder(order);
const payment = await this.processPayment(order);
await this.prepareOrder(order);
const shipment = await this.shipOrder(order);
await this.notifyCustomer(order, shipment);
return this.recordFulfillment(order);
}
async validateOrder(order) {
if (!order.items || order.items.length === 0) {
throw new Error("订单中没有商品");
}
}
async processPayment(order) {
throw new Error("必须实现processPayment方法");
}
async prepareOrder(order) {
console.log(`准备订单 #${order.id}`);
}
async shipOrder(order) {
throw new Error("必须实现shipOrder方法");
}
async notifyCustomer(order, shipment) {
console.log(`通知客户订单 #${order.id}已发货`);
}
async recordFulfillment(order) {
console.log(`记录订单 #${order.id}完成情况`);
return { success: true };
}
}
与其它模式的协同
模板方法模式常与其它模式结合使用:
- 工厂方法模式:模板方法中的某些步骤可以使用工厂方法创建对象
- 观察者模式:在钩子方法中实现发布/订阅机制
- 命令模式:将某些步骤封装为命令对象
class DocumentProcessor {
constructor() {
this.commands = [];
}
process(document) {
this.beforeProcessing(document);
this.setupCommands();
this.executeCommands(document);
this.afterProcessing(document);
}
beforeProcessing(document) {
console.log(`开始处理文档: ${document.name}`);
}
setupCommands() {
throw new Error("必须实现setupCommands方法");
}
executeCommands(document) {
this.commands.forEach(command => command.execute(document));
}
afterProcessing(document) {
console.log(`文档处理完成: ${document.name}`);
}
}
class PDFProcessor extends DocumentProcessor {
setupCommands() {
this.commands = [
new ValidateCommand(),
new ExtractTextCommand(),
new CompressCommand()
];
}
}