JavaScript设计模式———模板方法模式
定义: 在继承的基础上, 在父类中定义好执行的算法。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序****。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
例子:
泡茶和泡咖啡
来对比下泡茶和泡咖啡过程中的异同
步骤 泡茶 泡咖啡
1 烧开水 烧开水
2 浸泡茶叶 冲泡咖啡
3 倒入杯子 倒入杯子
4 加柠檬 加糖
const Drinks = function() {}
Drinks.prototype.firstStep = function() {
console.log('烧开水')
}
Drinks.prototype.secondStep = function() {}
Drinks.prototype.thirdStep = function() {
console.log('倒入杯子')
}
Drinks.prototype.fourthStep = function() {}
Drinks.prototype.init = function() { // 模板方法模式核心: 在父类上定义好执行算法
this.firstStep()
this.secondStep()
this.thirdStep()
this.fourthStep()
}
const Tea = function() {}
Tea.prototype = new Drinks
Tea.prototype.secondStep = function() {
console.log('浸泡茶叶')
}
Tea.prototype.fourthStep = function() {
console.log('加柠檬')
}
const Coffee = function() {}
Coffee.prototype = new Drinks
Coffee.prototype.secondStep = function() {
console.log('冲泡咖啡')
}
Coffee.prototype.fourthStep = function() {
console.log('加糖')
}
const tea = new Tea()
tea.init()
// 烧开水
// 浸泡茶叶
// 倒入杯子
// 加柠檬
const coffee = new Coffee()
coffee.init()
// 烧开水
// 冲泡咖啡
// 倒入杯子
// 加糖
JavaScript 没有抽象类的缺点和解决方案
-
JavaScript 并没有从语法层面提供对抽象类的支持。抽象类的第一个作用是隐藏对象的具体类型,由于 JavaScript 是一门“类型模糊”的语言,所以隐藏对象的类型在 JavaScript 中并不重要。
-
当我们在 JavaScript 中使用原型继承来模拟传统的类式继承时,并没有编译器帮助我们进行任何形式的检查,我们也没有办法保证子类会重写父类中的“抽象方法”。
下面提供两种变通的解决方案。
- 第 1 种方案是用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法。
但模拟接口检查会带来不必要的复杂性,而且要求程序员主动进行这些接口检查,这就要求我们在业务代码中添加一些跟业务逻辑无关的代码。 - 第 2 种方案是让 Beverage.prototype.brew 等方法直接抛出一个异常,如果因为粗心忘记编写 Coffee.prototype.brew 方法,那么至少我们会在程序运行时得到一个错误:
Beverage.prototype.brew = function(){
throw new Error( '子类必须重写 brew 方法' );
};
Beverage.prototype.pourInCup = function(){
throw new Error( '子类必须重写 pourInCup 方法' );
};
Beverage.prototype.addCondiments = function(){
throw new Error( '子类必须重写 addCondiments 方法' );
};
钩子方法
通过模板方法模式,我们在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?(如泡咖啡的例子中,客人需要不加糖的咖啡)
钩子方法( hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。
我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。
var Beverage = function() {};
Beverage.prototype.boilWater = function() {
console.log('把水煮沸');
};
Beverage.prototype.brew = function() {
throw new Error('子类必须重写 brew 方法');
};
Beverage.prototype.pourInCup = function() {
throw new Error('子类必须重写 pourInCup 方法');
};
Beverage.prototype.addCondiments = function() {
throw new Error('子类必须重写 addCondiments 方法');
};
// 钩子 是否挂上钩子由继承者来决定
Beverage.prototype.customerWantsCondiments = function() {
return true; // 默认需要调料
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) { // 如果挂钩返回 true,则需要调料
this.addCondiments();
}
};
var CoffeeWithHook = function() {};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function() {
console.log('用沸水冲泡咖啡');
};
CoffeeWithHook.prototype.pourInCup = function() {
console.log('把咖啡倒进杯子');
};
CoffeeWithHook.prototype.addCondiments = function() {
console.log('加糖和牛奶');
};
// 挂上勾子
CoffeeWithHook.prototype.customerWantsCondiments = function() {
return window.confirm('请问需要调料吗? ');
};
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
好莱坞原则
在这一原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”。
使用场景:
-
模板方法模式
当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。 -
发布—订阅模式
在发布—订阅模式中,发布者会把消息推送给订阅者,这取代了原先不断去 fetch 消息的形式。 -
回调函数
在 ajax 异步请求中,由于不知道请求返回的具体时间,而通过轮询去判断是否返回数据,这显然是不理智的行为。所以我们通常会把接下来的操作放在回调函数中,传入发起 ajax 异步请求的函数。当数据返回之后,这个回调函数才被执行,这也是好莱坞原则的一种体现。把需要执行的操作封装在回调函数里,然后把主动权交给另外一个函数。至于回调函数什么时候被执行,则是另外一个函数控制的。
总结
模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放-封闭原则的。
但在 JavaScript 中,我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)