JavaScript设计模式-----模板方法模式

模板方法模式是一种只需要使用继承就可以实现的非常简单点的模式。

模板方法模式有两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现

一些公共方法以及封装子类中所有的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

Coffee or Tea

泡咖啡的步骤通常如下:

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 把咖啡倒进杯子
  4. 加糖和牛奶

通过下面这段代码,就可以得到一杯香浓的咖啡:

var Coffee = function() {};

Coffee.prototype.boilWater = function() {
    console.log('把水煮沸');
}
Coffee.prototype.brewCofffeeGriends = function() {
    console.log('用沸水冲泡咖啡');
}
Coffee.prototype.pourInCup = function() {
    console.log('把咖啡倒进杯子');
}
Coffee.prototype.addSugarAndMilk = function() {
    console.log('加糖和牛奶');
}

Coffee.prototype.init = function() {
    this.boilWater();
    this.brewCofffeeGriends();
    this.pourInCup();
    this.addSugarAndMilk();
}

var coffee = new Coffee();
coffee.init();

泡茶的步骤大致如下:

var Tea = function() {};

Tea.prototype.boilWater = function() {
    console.log('把水煮沸');
}
Tea.prototype.steepTeaBag = function() {
    console.log('用沸水浸泡茶叶');
}
Tea.prototype.pourInCup = function() {
    console.log('把茶水倒进杯子');
}
Tea.prototype.addLemon = function() {
    console.log('加柠檬');
}

Tea.prototype.init = function() {
    this.boilWater();
    this.steepTeaBag();
    this.pourInCup();
    this.addLemon();
}

var tea = new Tea();
tea.init();

分离共同点

对泡咖啡和泡茶的过程进行抽象,可以得到如下过程:现象→抽象→模型

  1. 把水煮沸
  2. 谁沸水冲泡饮料
  3. 饮料倒进杯子
  4. 调料

用代码实现如下:

var Beverge = function() {};

Beverge.prototype.boilWater = function() {
    console.log('把水煮沸');
};
Beverge.prototype.brew = function() {};  //空方法,应该由子类重写

Beverge.prototype.pourInCup = function() {}; //空方法,应该由子类重写

Beverge.prototype.addCondiments = function() {}; //空方法,应该由子类重写

Beverge.prototype.init = function() {
    this.boilWater();
    this.brew();
    this.pourInCup();
}

创建Coffee和Tea子类

创建Beverage类的对象对我们来说没有意义,因为不存在真正一种叫饮料的。接下来创建咖啡类和茶类,并让他们继承饮料类:

var Coffee = function() {};

Coffee.prototype = new Beverge();
//重写抽象父类中的一些方法
Coffee.prototype.brew = function() {
    console.log('用沸水冲泡咖啡');
}

Coffee.prototype.pourInCup = function() {
    console.log('把咖啡倒进杯子');
}

Coffee.prototype.addCondiments = function() {
    console.log('加糖和牛奶');
}

var coffee = new Coffee();
coffee.init();

当调用coffee对象的init方法时,由于coffee对象和Coffee构造器的原型上都没有init方法,所以该请求会顺着原型链,被委托给Coffee的“父类”Beverage原型上的init方法。

而Beverage.prototype.init已经规定好了泡饮料的顺序和把水煮沸这一过程。

泡茶的过程类似。

而在泡茶和泡咖啡的过程中,Beverage.prototype.init就是我们讲的模板方法。

而在java中,则可直接通过抽象类来实现这一过程。

JavaScript没有抽象类的解决方案

javascript并没有从语法层面提供对抽象类的支持。

抽象类的第一个作用是隐藏对象的具体类型,由于JavaScript是一门“类型模糊”的语言,所以隐藏对象的类型在JavaScript中并不重要。

另一方面,当在JavaScript中使用原型继承来模拟传统的类继承时,没有编译器帮助我们进行任何形式的检查,也没有办法保证子类重写父类中的“抽象方法”。

下面提供两种解决方案:

  1. 用鸭子类型模拟接口检查,以确保子类中确实重写了父类的方法。缺点是会带来不必要的复杂性,而且要求程序员主动进行接口检查,要求在业务代码中增加与业务无关的代码。
  2. 让Beverage.prototype.brew等方法直接抛出异常,在忘记重写Coffee.prototype.brew方法时在程序运行时得到错误。
    Beverage.prototype.brew = function() {
        throw new  Error('子类必须重写brew方法');  
    }

    缺点是得到错误信息的时间点太靠后。

钩子方法

通过上述的步骤,制造出来的茶和咖啡已经可以满足大部分客户的需求了,但有一些客户喝咖啡不加调料(糖和牛奶)的。但是Beverage作为父类已经规定好了冲泡饮料的4个

步骤,怎么才能实现某些定制需求呢?

钩子方法(hook)可以解决这个问题,放置钩子是一种隔离变化的常见手段。在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,由子类自行决定。

代码做如下调整:

var Beverge = function() {};

Beverge.prototype.boilWater = function() {
    console.log('把水煮沸');
};
Beverge.prototype.brew = function() {
    throw new Error('子类必须重写brew方法');
}; 

Beverge.prototype.pourInCup = function() {
    throw new Error('子类必须重写pourInCup方法');
}; 

Beverge.prototype.addCondiments = function() {
    throw new Error('子类必须重写addCondiments方法');
}; 

Beverge.prototype.init = function() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    if (this.customerWantsCondiments()) {
        this.addCondiments();
    }
}

var CoffeeWithHook = function() {};

CoffeeWithHook.prototype = new Beverge();
//重写抽象父类中的一些方法
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 coffee = new CoffeeWithHook();
coffee.init();

真的需要“继承”吗

模板方法时为数不多的基于继承的设计模式,但JavaScript实际上没有提供真正的类式继承。

由于JavaScript的灵活性,实现这样的例子不一定非继承不可。

在好莱坞原则的指导下,可以这样实现:

var Beverge = function(param) {

    var boilWater = function() {
        console.log('把水煮沸');
    };

    var brew = param.brew || function() {
        throw new Error('必须传递brew方法');
    }

    var pourInCup = param.pourInCup || function() {
        throw new Error('必须传递pourInCup方法');
    }

    var addCondiments = param.addCondiments ||function() {
        throw new Error('必须传递addCondiments方法');
    }

    var F = function() {};

    F.prototype.init = function() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    return F;
}

var Coffee = Beverge({
    brew: function() {
        console.log('用沸水冲泡咖啡');
    },
    pourInCup:function() {
        console.log('把咖啡倒进杯子');
    },
    addCondiments: function() {
        console.log('加糖和牛奶');
    }
});

var coffee = new Coffee();
coffee.init();

小结

在JavaScript中,我们很多时候都不需要依样画瓢去实现一个模板方法模式,高阶函数是更好的方式。