Js中的模板方法模式
定义
定义操作执行中方法的骨架,使所有子类按照定义好的步骤执行,但是具体执行的细节放在子类中实现的一种模式。
详细描述
模板方法模式是一种高层决策,基层执行模式,就像在公司中领导指定战略步骤,基层人员跟着指定好的步骤具体去执行一些事情。
它由两种角色高层(抽象父类)和基层(具体执行子类)组成。抽象父类中主要含有抽象方法、钩子方法、公共方法和定义算法骨架的模板方法。子类中主要是具体执行方法和钩子方法,他们都是对父类中已经定义好的方法的覆盖。
模板方法模式采用好莱坞原则(你别找我,我会找你)实现,也就是说子类中实现的具体执行方法后,只需要等待父类去调用即可。何时调用如何调用全部由高层模板来决定。
代码实例
想象一下在我们日常生活中做饭的步骤,可以抽象为如下几大步:
1、 开火
2、 开始做饭
3、 做好装盘
下面我们就根据抽象出的步骤来实现我们的父类,并且定义子类来具体实现不同饭菜的完成:
// 父类
class Cook {
// 开火
fireStarter() {
console.log('我把火打开了');
}
// 做饭
cooking() {}
// 装盘
putPlate() {}
// 模板
init() {
this.fireStarter();
this.cooking();
this.putPlate();
}
}
// 子类
class spareribs extends Cook {
// 具体执行细节
cooking() {
console.log('开始做饭了');
console.log('做好了一份大排骨');
}
//
putPlate() {
console.log('做好了装到盆里');
}
}
const rib = new spareribs();
rib.init();
// 子类
class Fish extends Cook {
// 具体执行细节
cooking() {
console.log('开始做饭了');
console.log('放点香菜');
console.log('做好了一份鱼');
}
//
putPlate() {
console.log('做好了装到盘子里');
}
}
const fish = new Fish();
fish.init();
上述代码中定义了一个抽象父类,里面定义了我们业务执行的步骤方法,然后通过模板init方法确定了方法执行的步骤,这就是模板方法主要体现。然后通过父类定义好子类,子类内覆盖了父类中抽象的方法,里面编写具体业务的执行细节(如:做饭的时候材料和方式都可以自己选择)最后整体执行的步骤还是取决于父类模板方法中指定的骨架。当子类中有共同操作时,就提取到父类中统一实现不在单独覆盖(如:点火操作)来提高代码复用率。
下面看执行效果:
钩子方法
钩子方法主要是决定父类中定义的模板中的步骤要不要知执行的方法,其目的就是子类可以根据自身情况干预步骤执行。
假如我在做好排骨后不想装在盘子里,我直接用锅吃,此时我们就可以定义一个钩子函数来忽略装盘这一行为,代码如下:
// 父类
class Cook {
// 开火
fireStarter() {
console.log('我把火打开了');
}
// 做饭
cooking() {}
// 装盘
putPlate() {}
// 钩子函数
isputPlate() {
return true;
}
// 模板
init() {
this.fireStarter();
this.cooking();
this.isputPlate() && this.putPlate();
}
}
// 子类
class spareribs extends Cook {
// 具体执行细节
cooking() {
console.log('开始做饭了');
console.log('做好了一份大排骨');
}
//
putPlate() {
console.log('做好了装到盆里');
}
// 钩子函数
isputPlate() {
return false;
}
}
const rib = new spareribs();
rib.init();
上述代码就是定义了isputPlate钩子函数然后由子类根据情况来决定是否还进行装盘,这样子类就实现了根据自身情况在一定程度下干预模板步骤的执行。
函数实现模板方法
在js中函数作为一等公民,并且可以传来传去等语言特点下,使我们想要实现模板方法不需要非得使用类来进行实现,使用高阶函数也完全可以,并且还更加的轻量化。下面看下js中函数来实现模板方法模式:
function Cook(params) {
const fnData = {
fireStarter() {
console.log('我把火打开了');
},
cooking() {},
putPlate() {},
// 钩子函数
isputPlate() {
return true;
},
...params,
};
// 模板方法
const Fn = function () {
fnData.fireStarter();
fnData.cooking();
fnData.isputPlate() && fnData.putPlate();
};
return Fn;
}
const Spareribs = Cook({
// 具体执行细节
cooking() {
console.log('开始做饭了');
console.log('做好了一份大排骨');
},
putPlate() {
console.log('做好了装到盆里');
},
// 钩子函数
isputPlate() {
return false;
},
});
Spareribs();
const Fish = Cook({
// 具体执行细节
cooking() {
console.log('开始做饭了');
console.log('放点香菜');
console.log('做好了一份鱼');
},
putPlate() {
console.log('做好了装到盘子里');
},
});
Fish();
上述代码使用函数实现了和类似类继承相似的模板方法。我们在使用的时候可以根据自己的业务场景来选择实现的方式。打印效果如下:
使用场景
根据上面的描述和实例可以看出,当我们有固定的执行流程,各个子类都遵守这个流程的时候我们就可以使用模板方法来实现。
比如:如组件加载的生命周期、代码构建过程等等。
总结
优点
1、 封装了不可变的地方,将可变的部分由子类继承实现,更加易于扩展
2、 底层和执行层隔离开,降低耦合度
3、 将公共的代码进行提升,提高代码复用率
缺点
1、 由于使用继承来实现,如果父类添加新的抽象方法,则所有子类都要改一遍
2、 每个实现都要定义一个新类,会使系统更加复杂庞大
3、 因为每个每个方法实现更加抽象,会使代码更加难以理解
每件事务都有两面性更加高效方便的同时也会带来其他的一些副作用,这就需要我们了解他们的特性,在合适的时间合适的场景下去使用它们。