这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战
介绍
本期会向介绍一种典型的通过封装变化提高系统扩展性的设计模式——模板方法模式,后面将会给通过一个有趣的小案例来让大家去更快的了解它,同时会学到ES5和ES6的一些继承方法。
概念
一种只需使用继承就可以实现的非常简单的模式,一种严重依赖抽象类的设计模式。一般由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
继承
通过刚才的概念我们会了解到,模板方法模式高度依赖继承,那么我们先来分别用ES5和ES6的方式去完成一个类的继承。
ES5继承
var Pokemon = function(name){
this.name = name;
}
Pokemon.prototype.eat = function(){
console.log("eat food");
}
var Pikachu = function () { };
Pikachu.prototype = new Pokemon("皮卡丘");
Pikachu.prototype.eat = function () {
console.log("eat fruits");
}
众所周知,js的继承是天生自带的,只要是用原型链就可以完成,如果Pikachu想继承Pokemon抽象类,那么只需要在Pikachu.prototype上去实例化Pokemon抽象类就可以完成继承操作。
ES6继承
class Pokemon {
constructor(name) {
this.name = name;
}
eat() {
console.log("eat food");
}
}
class Pikachu extends Pokemon{
constructor(){
super("皮卡丘")
}
eat(){
console.log("eat fruits");
}
}
同样是继承,很明显ES6语法糖与ES5对比,其书写相当简洁,用extends关键词和super(),也令人一目了然,后端的小伙伴都可以一下看懂。
案例
我们将要通过模板方法模式来实现一个制作孜然羊肉的小案例。
class Food {
constructor(name) {
this.name = name;
}
wash() {
console.log("清洗" + this.name);
}
put() {
throw new Error("需要写子类方法put");
}
souse() {
throw new Error("需要写子类方法souse");
}
heating() {
throw new Error("需要写子类方法heating");
}
join() {
throw new Error("需要写子类方法join");
}
isSouse() {
return false;
}
start() {
this.wash();
if (this.isSouse()) {
this.souse();
}
this.put();
this.heating();
this.join();
}
}
羊肉作为食物,那么就要先准备他的抽象类Food,后面的羊肉要继承他。但我们仅通过Food类就可以看出作为模板的流程,wash()清洗,put()放到某某容器,heating()加热多久,join()加入什么佐料,在start()里可以看到就按一定顺序执行了。但是我们再仔细看那句if和它里面的souse(),这里就是我们常说钩子函数或者叫钩子方法。钩子方法(hook)的目的是来解决内部变化的问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能。
接下来,我们就可以来研究研究,羊肉怎么继承了。
class Mutton extends Food {
constructor(){
super("羊肉")
}
put() {
console.log("将" + this.name + "放入烤箱中");
}
heating() {
console.log("烤半小时");
}
join() {
console.log("加入孜然");
}
isSouse() {
return window.confirm(this.name + "需要腌制吗?");
}
souse() {
console.log("加入姜和食盐腌制1小时");
}
}
这里我们可以看到抽象类的方法可以被重写,有的方法如start()不用重新直接复用抽象父级,至于钩子函数会询问要不要腌制一下,如果是的话就要再腌制一会才会继续走下面的方法。
let mutton = new Mutton();
mutton.start();
现在,我们就可以做孜然羊肉大餐了~
结语
在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。但在js中,出了写一些框架和特定需求使用的并不是太频繁,而且我们很多时候都不需要照葫芦画瓢用模版方法模式,完全可以用高阶函数去实现需求。但,js继承这部分内容确是非常重要的。