大话模板方法模式

259 阅读4分钟

这是我参与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();

现在,我们就可以做孜然羊肉大餐了~

微信截图_20220206181355.png

微信截图_20220206181432.png

结语

在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。但在js中,出了写一些框架和特定需求使用的并不是太频繁,而且我们很多时候都不需要照葫芦画瓢用模版方法模式,完全可以用高阶函数去实现需求。但,js继承这部分内容确是非常重要的。