前端开发那些事儿

JS设计模式之模板模式

2021-05-20  本文已影响0人  Splendid飞羽

[#]什么是模板模式?

模板模式是:抽象父类定义了子类需要重写的相关方法。并且这些方法,仍然是通过父类方法调用的。

根据描述,父类提供了“模板”并决定是否调用,子类进行具体实现。
[图片上传中...(image.png-cd26e1-1621520608105-0)]

/* 饮料类,父类 */
class Beverage {
    constructor() {
        if (new.target === Beverage) {
            throw new Error('抽象类不能直接实例化!')
        }
    }
  
    /* 烧开水,共用方法 */
    boilWater() { console.log('水已经煮沸') }
    
    /* 冲泡饮料,抽象方法 */
    brewDrink() { throw new Error('抽象方法不能调用!') }
    
    /* 倒杯子里,共用方法 */
    pourCup() { console.log('倒进杯子里') }
    
    /* 加调味品,抽象方法 */
    addCondiment() { throw new Error('抽象方法不能调用!') }
    
    /* 制作流程,模板方法 */
    init() {
        this.boilWater()
        this.brewDrink()
        this.pourCup()
        this.addCondiment()
    }
}

/* 咖啡类,子类 */
class Coffee extends Beverage {
    constructor() { super() }
    
    /* 冲泡饮料,实现抽象方法 */
    brewDrink() { console.log('冲泡咖啡') }
    
    /* 加调味品,实现抽象方法 */
    addCondiment() { console.log('加点咖啡伴侣') }
}

const coffee = new Coffee()
coffee.init()

// 输出:水已经煮沸
// 输出:冲泡咖啡
// 输出:倒进杯子里
// 输出:加点咖啡伴侣

如果需要创建一个新的饮料,那么增加一个新的实例类,并实现父类中的抽象方法。如果不实现就去调用 init 方法即报错:

// 接上一段代码
/* 茶类,子类 */
class Tea extends Beverage {
    constructor() { super() }
    
    /* 冲泡饮料,实现抽象方法 */
    brewDrink() { console.log('冲泡茶') }
    
    /* 注意这里,没有实现加调味品抽象方法 */
}

const tea = new Tea()
tea.init()

// 输出:水已经煮沸
// 输出:冲泡茶
// 输出:倒进杯子里
// Error: 抽象方法不能调用!

模板方法模式的通用实现

根据上面的例子,我们可以提炼一下模板方法模式。饮料类可以被认为是父类(AbstractClass),父类中实现了模板方法(templateMethod),模板方法中抽象了操作的流程,共用的操作流程是普通方法,而非共用的可变方法是抽象方法,需要被子类(ConcreteClass)实现,或者说覆盖,子类在实例化后执行模板方法,就可以按照模板方法定义好的算法一步步执行。主要有下面几个概念:

AbstractClass :抽象父类,把一些共用的方法提取出来,把可变的方法作为抽象类,最重要的是把算法骨架抽象出来为模板方法;
templateMethod :模板方法,固定了希望执行的算法骨架;
ConcreteClass :子类,实现抽象父类中定义的抽象方法,调用继承的模板方法时,将执行模板方法中定义的算法流程;
下面用通用的方法实现,这里直接用 class 语法:

/* 抽象父类 */
class AbstractClass {
    constructor() {
        if (new.target === AbstractClass) {
            throw new Error('抽象类不能直接实例化!')
        }
    }
    
    /* 共用方法 */
    operate1() { console.log('operate1') }
    
    /* 抽象方法 */
    operate2() { throw new Error('抽象方法不能调用!') }
    
    /* 模板方法 */
    templateMethod() {
        this.operate1()
        this.operate2()
    }
}

/* 实例子类,继承抽象父类 */
class ConcreteClass extends AbstractClass {
    constructor() { super() }
    
    /* 覆盖抽象方法 operate2 */
    operate2() { console.log('operate2') }
}

const instance = new ConcreteClass()
instance.templateMethod()

// 输出:operate1
// 输出:operate2
模板方法模式的优点:

封装了不变部分,扩展可变部分, 把算法中不变的部分封装到父类中直接实现,而可变的部分由子类继承后再具体实现; 提取了公共代码部分,易于维护, 因为公共的方法被提取到了父类,那么如果我们需要修改算法中不变的步骤时,不需要到每- 一个子类中去修改,只要改一下对应父类即可;
行为被父类的模板方法固定, 子类实例只负责执行模板方法,具备可扩展性,符合开闭原则;
模板方法模式的缺点:增加了系统复杂度,主要是增加了的抽象类和类间联系,需要做好文档工作;

模板方法模式的使用场景

如果知道一个算法所需的关键步骤,而且很明确这些步骤的执行顺序,但是具体的实现是未知的、灵活的,那么这时候就可以使用模板方法模式来将算法步骤的框架抽象出来;
重要而复杂的算法,可以把核心算法逻辑设计为模板方法,周边相关细节功能由各个子类实现;
模板方法模式可以被用来将子类组件将自己的方法挂钩到高层组件中,也就是钩子,子类组件中的方法交出控制权,高层组件在模板方法中决定何时回调子类组件中的方法,类似的用法场景还有发布-订阅模式、回调函数;

模板方法与其他设计模式区别

模板方法模式与工厂模式
模板方法模式的实现可以使用工厂模式来获取所需的对象。

模板方法模式和抽象工厂模式比较类似,都是使用抽象类来提取公共部分,不一样的是:
抽象工厂模式 提取的是实例的功能结构;
模板方法模式 提取的是算法的骨架结构;

上一篇 下一篇

猜你喜欢

热点阅读