您的位置:新葡亰496net > 奥门新萄京娱乐场 > 新葡亰496net:之模板方法格局,读书笔记09

新葡亰496net:之模板方法格局,读书笔记09

发布时间:2019-10-05 12:42编辑:奥门新萄京娱乐场浏览(130)

    本文的概念内容来自深入浅出设计模式一书.

    模板方法模式(Template)

      ——在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

    • 好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。
    • 要点:
    1. 模板方法的抽象类可以定义具体方法、抽象方法和钩子。抽象方法由子类实现。
    2. 钩子是一种方法,在抽象类中不做事,或只做默认的事,子类可以选择要不要覆盖它。
    3. 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
    4. 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用低层模块。
    5. 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
    6. 工厂方法(由子类决定实例化哪个具体类)是模板方法(子类决定如何实现算法中的步骤)的一种特殊版本。

     

    示例:

    前面的话

      在javascript开发中用到继承的场景其实并不是很多,很多时候喜欢用mix-in的方式给对象扩展属性。但这不代表继承在javascript里没有用武之地,虽然没有真正的类和继承机制,但可以通过原型prototype来变相地实现继承。本文将详细介绍一种基于继承的设计模式——模板方法(TemplateMethod)模式

     

    前面的话

      在javascript开发中用到继承的场景其实并不是很多,很多时候喜欢用mix-in的方式给对象扩展属性。但这不代表继承在javascript里没有用武之地,虽然没有真正的类和继承机制,但可以通过原型prototype来变相地实现继承。本文将详细介绍一种基于继承的设计模式——模板方法(TemplateMethod)模式

     

     

    项目需求

    有一家咖啡店, 供应咖啡和茶, 它们的工序如下:

    新葡亰496net 1

    咖啡:

    新葡亰496net 2

    茶:

    新葡亰496net 3

    可以看到咖啡和茶的制作工序是差不多的, 都是有4步, 其中有两步它们两个是一样的, 另外两步虽然具体内容不一样, 但是都做做的同一类工作.

    现在问题也有了, 当前的设计两个类里面有很多重复的代码, 那么应该怎样设计以减少冗余呢?

    咖啡冲泡法

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

    定义

      模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法

      假如有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想

     

    定义

      模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法

      假如有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想

     

    《Head First设计模式》 读书笔记09 模板方法模式

    初次尝试

    新葡亰496net 4

    把共有的方法放到父类里面, 把不同的方法放到子类里面.

    父类里面有一个抽象的prepareRecipe()方法[翻译为准备烹饪方法/制作方法], 然后在不同的子类里面有不同的实现. 也就是说每个子类都有自己制作饮料的方法.

    茶冲泡法

    1. 把水煮沸
    2. 用沸水冲泡茶叶
    3. 把茶倒进杯子
    4. 加柠檬

    茶和咖啡是如此得相似,似乎我们应该将共同的部分抽取出来,放进一个基类中。

     1 public abstract class CaffeineBeverage {
     2     // 现在,用同一个prepareRecipe()方法来处理茶和咖啡。
     3     // prepareRecipe()方法被声明为final,因为我们不希望子类覆盖这个方法
     4     // 我们将第2步和第4步泛化成为brew()和addCondiments()
     5     final void prepareRecipe() {
     6         boilWater();
     7         brew();
     8         pourInCup();
     9         addCondiments();
    10     }
    11 
    12     // 因为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,
    13     // 剩余的东西留给子类去操心
    14     abstract void addCondiments();
    15     abstract void brew();
    16 
    17     public void boilWater() {
    18         System.out.println("Boiling water");
    19     }
    20 
    21     public void pourInCup() {
    22         System.out.println("Pouring into cup");
    23     }
    24 }
    

    让我们细看抽象类是如何被定义的,包括了它内含的模板方法和原语操作。

     1 // 这就是我们的抽象类。它被声明为抽象,用来作为基类,其子类必须实现其操作
     2 public abstract class AbstractClass {
     3     // 这就是模板方法。它被声明为final,以免子类改变这个算法的顺序。
     4     final void templateMethod() {
     5         // 模板方法定义了一连串的步骤,每个步骤由一个方法代表
     6         primitiveOperation1();
     7         primitiveOperation2();
     8         concreteOperation();
     9     }
    10 
    11     // 在这个范例中有两个原语操作,具体子类必须实现它们
    12     abstract void primitiveOperation1();
    13     abstract void primitiveOperation2();
    14     
    15     // 这个抽象类有一个具体的操作。
    16     void concreteOperation() {
    17         // ...
    18     }
    19 }
    

     

    接着需要处理咖啡和茶类,这两个类现在都是依赖超类来处理冲泡法,所以只需要自行处理冲泡和添加调料部分:

     1 public class Coffee extends CaffeineBeverage {
     2     @Override
     3     void brew() {
     4         System.out.println("Dripping coffee through filter");
     5     }
     6 
     7     @Override
     8     void addCondiments() {
     9         System.out.println("Adding Sugar and Milk");
    10     }
    11 }
    12 
    13 public class Tea extends CaffeineBeverage {
    14     @Override
    15     void brew() {
    16         System.out.println("Steeping the tea");
    17     }
    18 
    19     @Override
    20     void addCondiments() {
    21         System.out.println("Adding Lemon");
    22     }
    23 }
    

    咖啡与茶

      咖啡与茶是一个经典的例子,经常用来讲解模板方法模式,这个例子的原型来自《HeadFirst设计模式》。下面用javascript来实现这个例子

      首先,先来泡一杯咖啡,如果没有什么太个性化的需求,泡咖啡的步骤通常如下:1、把水煮沸;2、用沸水冲泡咖啡;3、把咖啡倒进杯子;4、加糖和牛奶

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

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

      接下来,开始准备茶,泡茶的步骤跟泡咖啡的步骤相差并不大:1、把水煮沸;2、用沸水浸泡茶叶 3、把茶水倒进杯子;4、加柠檬

      下面用一段代码来实现泡茶的步骤:

    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();
    

      现在分别泡好了一杯咖啡和一壶茶,经过思考和比较,发现咖啡和茶的冲泡过程是大同小异的

    新葡亰496net 5

      泡咖啡和泡茶主要有以下不同点:

      1、原料不同。一个是咖啡,一个是茶,但可以把它们都抽象为“饮料”

      2、泡的方式不同。咖啡是冲泡,而茶叶是浸泡,可以把它们都抽象为“泡”

      3、加入的调料不同。一个是糖和牛奶,一个是柠檬,但可以把它们都抽象为“调料”

      经过抽象之后,不管是泡咖啡还是泡茶,都能整理为下面四步:

      1、把水煮沸

      2、用沸水冲泡饮料

      3、把饮料倒进杯子

      4、加调料

      所以,不管是冲泡还是浸泡,都能给它一个新的方法名称,比如说brew()。同理,不管是加糖和牛奶,还是加柠檬,都可以称之为addCondiments()

      现在可以创建一个抽象父类来表示泡一杯饮料的整个过程。不论是Coffee,还是Tea,都用Beverage来表示,代码如下:

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

     

    咖啡与茶

      咖啡与茶是一个经典的例子,经常用来讲解模板方法模式,这个例子的原型来自《HeadFirst设计模式》。下面用javascript来实现这个例子

      首先,先来泡一杯咖啡,如果没有什么太个性化的需求,泡咖啡的步骤通常如下:1、把水煮沸;2、用沸水冲泡咖啡;3、把咖啡倒进杯子;4、加糖和牛奶

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

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

      接下来,开始准备茶,泡茶的步骤跟泡咖啡的步骤相差并不大:1、把水煮沸;2、用沸水浸泡茶叶 3、把茶水倒进杯子;4、加柠檬

      下面用一段代码来实现泡茶的步骤:

    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();
    

      现在分别泡好了一杯咖啡和一壶茶,经过思考和比较,发现咖啡和茶的冲泡过程是大同小异的

    新葡亰496net 6

      泡咖啡和泡茶主要有以下不同点:

      1、原料不同。一个是咖啡,一个是茶,但可以把它们都抽象为“饮料”

      2、泡的方式不同。咖啡是冲泡,而茶叶是浸泡,可以把它们都抽象为“泡”

      3、加入的调料不同。一个是糖和牛奶,一个是柠檬,但可以把它们都抽象为“调料”

      经过抽象之后,不管是泡咖啡还是泡茶,都能整理为下面四步:

      1、把水煮沸

      2、用沸水冲泡饮料

      3、把饮料倒进杯子

      4、加调料

      所以,不管是冲泡还是浸泡,都能给它一个新的方法名称,比如说brew()。同理,不管是加糖和牛奶,还是加柠檬,都可以称之为addCondiments()

      现在可以创建一个抽象父类来表示泡一杯饮料的整个过程。不论是Coffee,还是Tea,都用Beverage来表示,代码如下:

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

     

    The Template Method Pattern

     

    再仔细想想应该怎样设计

    新葡亰496net 7

    可以发现两个饮料的制作方法遵循了同样的算法:

    1. 把水烧开
    2. 用开水冲咖啡或茶
    3. 把冲开的饮料放到杯里
    4. 添加适当的调料

    现在我们来抽像prepareRecipe()方法:

    1.先看看两个饮料的差异:

    新葡亰496net 8

    两种饮料都有四道工序, 两个是完全一样的, 另外两个在具体的实现上是略有不同的, 但是还是同样性质的工序.

    这两道不同的工序的本质就是冲饮料和添加调料, 所以prepareRecipe()可以这样写:

    新葡亰496net 9

    1. 把上面的方法放到超类里:

    新葡亰496net 10

    这个父类是抽象的, prepareRecipe()将会用来制作咖啡或者茶, 而且我不想让子类去重写这个方法, 因为制作工序(算法)是一定的.

    只不过里面的第2部和第4部是需要子类自己来实现的. 所以brew()和addCondiments()是两个抽象的方法, 而另外两个方法则直接在父类里面实现了.

    1. 最后茶和咖啡就是这个样子的:

    新葡亰496net 11

     

    新葡亰496net 12

     

    创建子类

      现在创建一个Beverage类的对象没有意义,因为世界上能喝的东西没有一种真正叫“饮料”的,饮料在这里还只是一个抽象的存在。接下来要创建咖啡类和茶类,并让它们继承饮料类:

    var Coffee = function(){};
    Coffee.prototype = new Beverage();
    

      接下来要重写抽象父类中的一些方法,只有“把水煮沸”这个行为可以直接使用父类Beverage中的boilWater方法,其他方法都需要在Coffee子类中重写,代码如下:

    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类完成了,当调用coffee对象的init方法时,由于coffee对象和Coffee构造器的原型prototype上都没有对应的init方法,所以该请求会顺着原型链,被委托给Coffee的“父类”Beverage原型上的init方法。而Beverage.prototype.init方法中已经规定好了泡饮料的顺序,所以能成功地泡出一杯咖啡

      接下来照葫芦画瓢,来创建Tea类:

    var Tea = function(){};
    Tea.prototype = new Beverage();
    Tea.prototype.brew = function(){
        console.log( '用沸水浸泡茶叶' );
    };
    Tea.prototype.pourInCup = function(){
        console.log( '把茶倒进杯子' );
    };
    Tea.prototype.addCondiments = function(){
        console.log( '加柠檬' );
    };
    var tea = new Tea();
    tea.init();
    

      本文讨论的是模板方法模式,那么在上面的例子中,到底谁才是所谓的模板方法呢?答案是Beverage.prototype.init。Beverage.prototype.init被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。在Beverage.prototype.init方法中,算法内的每一个步骤都清楚地展示在眼前

     

    创建子类

      现在创建一个Beverage类的对象没有意义,因为世界上能喝的东西没有一种真正叫“饮料”的,饮料在这里还只是一个抽象的存在。接下来要创建咖啡类和茶类,并让它们继承饮料类:

    var Coffee = function(){};
    Coffee.prototype = new Beverage();
    

      接下来要重写抽象父类中的一些方法,只有“把水煮沸”这个行为可以直接使用父类Beverage中的boilWater方法,其他方法都需要在Coffee子类中重写,代码如下:

    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类完成了,当调用coffee对象的init方法时,由于coffee对象和Coffee构造器的原型prototype上都没有对应的init方法,所以该请求会顺着原型链,被委托给Coffee的“父类”Beverage原型上的init方法。而Beverage.prototype.init方法中已经规定好了泡饮料的顺序,所以能成功地泡出一杯咖啡

      接下来照葫芦画瓢,来创建Tea类:

    var Tea = function(){};
    Tea.prototype = new Beverage();
    Tea.prototype.brew = function(){
        console.log( '用沸水浸泡茶叶' );
    };
    Tea.prototype.pourInCup = function(){
        console.log( '把茶倒进杯子' );
    };
    Tea.prototype.addCondiments = function(){
        console.log( '加柠檬' );
    };
    var tea = new Tea();
    tea.init();
    

      本文讨论的是模板方法模式,那么在上面的例子中,到底谁才是所谓的模板方法呢?答案是Beverage.prototype.init。Beverage.prototype.init被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。在Beverage.prototype.init方法中,算法内的每一个步骤都清楚地展示在眼前

     

    问题引入

      咖啡和茶的冲泡步骤都差不多,可以理解为两份冲泡法都采用了基本相同的算法:

        1.煮沸水。

        2.用热水泡茶或咖啡。

        3.把饮料倒进杯子。

        4.加入适当调料(奶、糖或者柠檬片)。

      如果实现不好,就会有重复的代码,算法的知识和实现会分散在许多类中,算法修改不容易,并且加入新种类的饮料也需要做很多工作。

     

      那该怎么设计呢?

      采用一个新的基类(咖啡因饮料类),其中有一个声明为final的方法(不希望被子类覆盖),为冲泡饮料的动作。

      该方法(也就是模板方法)中包含了若干个小方法调用,这些小方法就是一个个基本的步骤。

      对于这些步骤的处理:对各个类相同的子步骤在基类定义;而咖啡和茶有区别的步骤,声明为虚函数,依赖子类(咖啡类和茶类)自己去完成。

     新葡亰496net 13

     

      上面的图中,prepareRecipe()就是我们的模板方法

      它用作一个算法的模板,在这个模板中,算法内的每一个步骤都被一个方法代表了。

      某些方法是由超类处理的,某些方法留给子类处理,这些需要由子类提供的方法,必须在超类中声明为抽象。

     

      模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

    我们做了什么?

    我们意识到两种饮料的工序大体是一致的, 尽管某些工序需要不同的实现方法. 所以我们把这些饮料的制作方法归纳到了一个基类CaffeineBeverage里面.

    CaffeineBeverage控制着整个工序, 第1, 3部由它自己完成, 第2, 4步则是由具体的饮料子类来完成.

    钩子的使用

     1 public abstract class CaffeineBeverageWithHook {
     2     final void prepareRecipe() {
     3         boilWater();
     4         brew();
     5         pourInCup();
     6         // 我们加上了一个小小的条件语句,而该条件是否成立,
     7         // 是由一个具体方法customerWantsCondiments()决定的。
     8         // 如果顾客“想要”调料,只有这时我们才调用addCondiments()。
     9         if (customerWantsCondiments()) {
    10             addCondiments();
    11         }
    12     }
    13 
    14     abstract void addCondiments();
    15     abstract void brew();
    16 
    17     public void boilWater() {
    18         System.out.println("Boiling water");
    19     }
    20 
    21     public void pourInCup() {
    22         System.out.println("Pouring into cup");
    23     }
    24 
    25     // 我们在这里定义了一个方法,(通常)是空的缺省实现。这个方法只会返回true,不做别的事。
    26     // 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做。
    27     boolean customerWantsCondiments() {
    28         return true;
    29     }
    30 }
    

    抽象类

      模板方法模式是一种严重依赖抽象类的设计模式。javascript在语言层面并没有提供对抽象类的支持,也很难模拟抽象类的实现

      在Java中,类分为两种,一种为具体类,另一种为抽象类。具体类可以被实例化,抽象类不能被实例化。要了解抽象类不能被实例化的原因,可以思考“饮料”这个抽象类

      想象这样一个场景:口渴了去便利店想买一瓶饮料,不能直接跟店员说:“来一瓶饮料”。如果这样说了,那么店员接下来肯定会问:“要什么饮料?”饮料只是一个抽象名词,只有当真正明确了的饮料类型之后,才能得到一杯咖啡、茶或者可乐

      由于抽象类不能被实例化,如果有人编写了一个抽象类,那么这个抽象类一定是用来被某些具体类继承的。抽象类表示一种契约。继承了这个抽象类的所有子类都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为它的子类定义这些公共接口。如果在子类中删掉了这些方法中的某一个,那么将不能通过编译器的检查,这在某些场景下是非常有用的

      Beverage类的init方法里规定了冲泡一杯饮料的顺序如下:

    this.boilWater();    //把水煮沸
    this.brew();    //用水泡原料
    this.pourInCup();    //把原料倒进杯子
    this.addCondiments();        //添加调料
    

      如果在Coffee子类中没有实现对应的brew方法,那么百分之百得不到一杯咖啡。既然父类规定了子类的方法和执行这些方法的顺序,子类就应该拥有这些方法,并且提供正确的实现

      抽象方法被声明在抽象类中,抽象方法并没有具体的实现过程,是一些“哑”方法。比如Beverage类中的brew方法、pourInCup方法和addCondiments方法,都被声明为抽象方法。当子类继承了这个抽象类时,必须重写父类的抽象方法

      除了抽象方法之外,如果每个子类中都有一些同样的具体实现方法,那这些方法也可以选择放在抽象类中,这可以节省代码以达到复用的效果,这些方法叫作具体方法。当代码需要改变时,只需要改动抽象类里的具体方法就可以了。比如饮料中的boilWater方法,假设冲泡所有的饮料之前,都要先把水煮沸,那自然可以把boilWater方法放在抽象类Beverage中

      下面尝试着把Coffee和Tea的例子换成Java代码,这有助于理解抽象类的意义

    //Java代码
    public abstract class Beverage{    //饮料抽象类
      final void init(){    //模板方法
        boilWater();
        brew();
        pourInCup();
        addCondiments();
      }
    
      void boilWater(){    //具体方法
        boilWaterSystem.out.println("把水煮沸");
      }
    
      abstract void brew();    //抽象方法brew
      abstract void addCondiments();        //抽象方法addCondiments
      abstract void pourInCup();    //抽象方法pourInCup
    }
    
    public class Coffee extends Beverage{    //Coffee类
      @Override
      void brew(){    //子类中重写brew方法
        System.out.println("用沸水冲泡咖啡");
      }
      @Override
      void pourInCup(){    //子类中重写pourInCup方法
        System.out.println("把咖啡倒进杯子");
      }
      @Override
      void addCondiments(){    //子类中重写addCondiments方法
        System.out.println("加糖和牛奶");
      }
    }
    
    public class Tea extends Beverage{    //Tea类
      @Override
      voidbrew(){    //子类中重写brew方法
        System.out.println("用沸水浸泡茶叶");
      }
    
      @Override
      voidpourInCup(){    //子类中重写pourInCup方法
        System.out.println("把茶倒进杯子");
      }
    
      @Override
      voidaddCondiments(){    //子类中重写addCondiments方法
        System.out.println("加柠檬");
      }
    }
    
    public class Test{
      private static void prepareRecipe(Beveragebeverage){
        beverage.init();
      }
    
      public static void main(Stringargs[]){
        Beverage coffee = new Coffee();    //创建coffee对象
        prepareRecipe(coffee);    //开始泡咖啡
        //把水煮沸
        //用沸水冲泡咖啡
        //把咖啡倒进杯子
        //加糖和牛奶
      Beverage tea = new Tea();    //创建tea对象
      prepareRecipe(tea);    //开始泡茶
        //把水煮沸
        //用沸水浸泡茶叶
        //把茶倒进杯子
        //加柠檬
      }
    }
    

      javascript并没有从语法层面提供对抽象类的支持。抽象类的第一个作用是隐藏对象的具体类型,由于javascript是一门“类型模糊”的语言,所以隐藏对象的类型在javascript中并不重要。另一方面,在javascript中使用原型继承来模拟传统的类式继承时,并没有编译器帮助进行任何形式的检查,没有办法保证子类会重写父类中的“抽象方法”

      Beverage.prototype.init方法作为模板方法,已经规定了子类的算法框架,代码如下:

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

      如果Coffee类或者Tea类忘记实现这4个方法中的一个呢?拿brew方法举例,如果忘记编写Coffee.prototype.brew方法,那么当请求coffee对象的brew时,请求会顺着原型链找到Beverage“父类”对应的Beverage.prototype.brew方法,而Beverage.prototype.brew方法到目前为止是一个空方法,这显然是不符需要的

      在Java中编译器会保证子类会重写父类中的抽象方法,但在javascript中却没有进行这些检查工作。在编写代码的时候得不到任何形式的警告,完全寄托于程序员的记忆力和自觉性是很危险的,特别是当使用模板方法模式这种完全依赖继承而实现的设计模式时

      下面提供两种变通的解决方案:第1种方案是用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法。但模拟接口检查会带来不必要的复杂性,而且要求程序员主动进行这些接口检查,这就要求在业务代码中添加一些跟业务逻辑无关的代码;第2种方案是让Beverage.prototype.brew等方法直接抛出一个异常,如果因为粗心忘记编写Coffee.prototype.brew方法,那么至少会在程序运行时得到一个错误

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

      第2种解决方案的优点是实现简单,付出的额外代价很少;缺点是得到错误信息的时间点太靠后。一共有3次机会得到这个错误信息,第1次是在编写代码的时候,通过编译器的检查来得到错误信息;第2次是在创建对象的时候用鸭子类型来进行“接口检查”;而目前不得不利用最后一次机会,在程序运行过程中才知道哪里发生了错误

    【使用场景】

      从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空

      在Web开发中能找到很多模板方法模式的适用场景,比如在构建一系列的UI组件,这些组件的构建过程一般如下所示:

      1、初始化一个div容器

      2、通过ajax请求拉取相应的数据;

      3、把数据渲染到div容器里面,完成组件的构造;

      4、通知用户组件渲染完毕

      任何组件的构建都遵循上面的4步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是请求ajax的远程地址,第(3)步不同的地方是渲染数据的方式。于是可以把这4个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第(1)步和第(4)步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第(2)步和第(3)步

     

    抽象类

      模板方法模式是一种严重依赖抽象类的设计模式。javascript在语言层面并没有提供对抽象类的支持,也很难模拟抽象类的实现

      在Java中,类分为两种,一种为具体类,另一种为抽象类。具体类可以被实例化,抽象类不能被实例化。要了解抽象类不能被实例化的原因,可以思考“饮料”这个抽象类

      想象这样一个场景:口渴了去便利店想买一瓶饮料,不能直接跟店员说:“来一瓶饮料”。如果这样说了,那么店员接下来肯定会问:“要什么饮料?”饮料只是一个抽象名词,只有当真正明确了的饮料类型之后,才能得到一杯咖啡、茶或者可乐

      由于抽象类不能被实例化,如果有人编写了一个抽象类,那么这个抽象类一定是用来被某些具体类继承的。抽象类表示一种契约。继承了这个抽象类的所有子类都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为它的子类定义这些公共接口。如果在子类中删掉了这些方法中的某一个,那么将不能通过编译器的检查,这在某些场景下是非常有用的

      Beverage类的init方法里规定了冲泡一杯饮料的顺序如下:

    this.boilWater();    //把水煮沸
    this.brew();    //用水泡原料
    this.pourInCup();    //把原料倒进杯子
    this.addCondiments();        //添加调料
    

      如果在Coffee子类中没有实现对应的brew方法,那么百分之百得不到一杯咖啡。既然父类规定了子类的方法和执行这些方法的顺序,子类就应该拥有这些方法,并且提供正确的实现

      抽象方法被声明在抽象类中,抽象方法并没有具体的实现过程,是一些“哑”方法。比如Beverage类中的brew方法、pourInCup方法和addCondiments方法,都被声明为抽象方法。当子类继承了这个抽象类时,必须重写父类的抽象方法

      除了抽象方法之外,如果每个子类中都有一些同样的具体实现方法,那这些方法也可以选择放在抽象类中,这可以节省代码以达到复用的效果,这些方法叫作具体方法。当代码需要改变时,只需要改动抽象类里的具体方法就可以了。比如饮料中的boilWater方法,假设冲泡所有的饮料之前,都要先把水煮沸,那自然可以把boilWater方法放在抽象类Beverage中

      下面尝试着把Coffee和Tea的例子换成Java代码,这有助于理解抽象类的意义

    //Java代码
    public abstract class Beverage{    //饮料抽象类
      final void init(){    //模板方法
        boilWater();
        brew();
        pourInCup();
        addCondiments();
      }
    
      void boilWater(){    //具体方法
        boilWaterSystem.out.println("把水煮沸");
      }
    
      abstract void brew();    //抽象方法brew
      abstract void addCondiments();        //抽象方法addCondiments
      abstract void pourInCup();    //抽象方法pourInCup
    }
    
    public class Coffee extends Beverage{    //Coffee类
      @Override
      void brew(){    //子类中重写brew方法
        System.out.println("用沸水冲泡咖啡");
      }
      @Override
      void pourInCup(){    //子类中重写pourInCup方法
        System.out.println("把咖啡倒进杯子");
      }
      @Override
      void addCondiments(){    //子类中重写addCondiments方法
        System.out.println("加糖和牛奶");
      }
    }
    
    public class Tea extends Beverage{    //Tea类
      @Override
      voidbrew(){    //子类中重写brew方法
        System.out.println("用沸水浸泡茶叶");
      }
    
      @Override
      voidpourInCup(){    //子类中重写pourInCup方法
        System.out.println("把茶倒进杯子");
      }
    
      @Override
      voidaddCondiments(){    //子类中重写addCondiments方法
        System.out.println("加柠檬");
      }
    }
    
    public class Test{
      private static void prepareRecipe(Beveragebeverage){
        beverage.init();
      }
    
      public static void main(Stringargs[]){
        Beverage coffee = new Coffee();    //创建coffee对象
        prepareRecipe(coffee);    //开始泡咖啡
        //把水煮沸
        //用沸水冲泡咖啡
        //把咖啡倒进杯子
        //加糖和牛奶
      Beverage tea = new Tea();    //创建tea对象
      prepareRecipe(tea);    //开始泡茶
        //把水煮沸
        //用沸水浸泡茶叶
        //把茶倒进杯子
        //加柠檬
      }
    }
    

      javascript并没有从语法层面提供对抽象类的支持。抽象类的第一个作用是隐藏对象的具体类型,由于javascript是一门“类型模糊”的语言,所以隐藏对象的类型在javascript中并不重要。另一方面,在javascript中使用原型继承来模拟传统的类式继承时,并没有编译器帮助进行任何形式的检查,没有办法保证子类会重写父类中的“抽象方法”

      Beverage.prototype.init方法作为模板方法,已经规定了子类的算法框架,代码如下:

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

      如果Coffee类或者Tea类忘记实现这4个方法中的一个呢?拿brew方法举例,如果忘记编写Coffee.prototype.brew方法,那么当请求coffee对象的brew时,请求会顺着原型链找到Beverage“父类”对应的Beverage.prototype.brew方法,而Beverage.prototype.brew方法到目前为止是一个空方法,这显然是不符需要的

      在Java中编译器会保证子类会重写父类中的抽象方法,但在javascript中却没有进行这些检查工作。在编写代码的时候得不到任何形式的警告,完全寄托于程序员的记忆力和自觉性是很危险的,特别是当使用模板方法模式这种完全依赖继承而实现的设计模式时

      下面提供两种变通的解决方案:第1种方案是用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法。但模拟接口检查会带来不必要的复杂性,而且要求程序员主动进行这些接口检查,这就要求在业务代码中添加一些跟业务逻辑无关的代码;第2种方案是让Beverage.prototype.brew等方法直接抛出一个异常,如果因为粗心忘记编写Coffee.prototype.brew方法,那么至少会在程序运行时得到一个错误

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

      第2种解决方案的优点是实现简单,付出的额外代价很少;缺点是得到错误信息的时间点太靠后。一共有3次机会得到这个错误信息,第1次是在编写代码的时候,通过编译器的检查来得到错误信息;第2次是在创建对象的时候用鸭子类型来进行“接口检查”;而目前不得不利用最后一次机会,在程序运行过程中才知道哪里发生了错误

    【使用场景】

      从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空

      在Web开发中能找到很多模板方法模式的适用场景,比如在构建一系列的UI组件,这些组件的构建过程一般如下所示:

      1、初始化一个div容器

      2、通过ajax请求拉取相应的数据;

      3、把数据渲染到div容器里面,完成组件的构造;

      4、通知用户组件渲染完毕

      任何组件的构建都遵循上面的4步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是请求ajax的远程地址,第(3)步不同的地方是渲染数据的方式。于是可以把这4个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第(1)步和第(4)步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第(2)步和第(3)步

     

     

    初识模板方法模式

    新葡亰496net 14

    上面的需求种, prepareRecipe() 就是模板方法. 因为, 它首先是一个方法, 然后它还充当了算法模板的角色, 这个需求里, 算法就是制作饮料的整个工序.

    所以说: 模板方法定义了一个算法的步骤, 并允许子类提供其中若干个步骤的具体实现.

     


    钩子方法

      通过模板方法模式,在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?比如在饮料类Beverage中封装了饮料的冲泡顺序:

      1、把水煮沸

      2、用沸水冲泡饮料

      3、把饮料倒进杯子

      4、加调料

      这4个冲泡饮料的步骤适用于咖啡和茶,在饮料店里,根据这4个步骤制作出来的咖啡和茶,一直顺利地提供给绝大部分客人享用。但有一些客人喝咖啡是不加调料(糖和牛奶)的。既然Beverage作为父类,已经规定好了冲泡饮料的4个步骤,那么有什么办法可以让子类不受这个约束呢?

      钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能

      在下面这个例子里,把挂钩的名字定为customerWantsCondiments,接下来将挂钩放入Beverage类,看看如何得到一杯不需要糖和牛奶的咖啡,代码如下:

    var Beverage = function(){};
    Beverage.prototype.boilWater = function(){
        console.log( '把水煮沸' );
    };
    Beverage.prototype.brew = function(){
        throw new Error( '子类必须重写brew 方法' );
    };
    Beverage.prototype.pourInCup = function(){
        throw new Error( '子类必须重写pourInCup 方法' );
    };
    Beverage.prototype.addCondiments = function(){
        throw new Error( '子类必须重写addCondiments 方法' );
    };
    Beverage.prototype.customerWantsCondiments = function(){
        return true; // 默认需要调料
    };
    Beverage.prototype.init = function(){
        this.boilWater();
        this.brew();
        this.pourInCup();
        if ( this.customerWantsCondiments() ){ // 如果挂钩返回true,则需要调料
            this.addCondiments();
        }
    };
    
    var CoffeeWithHook = function(){};
    CoffeeWithHook.prototype = new Beverage();
    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 coffeeWithHook = new CoffeeWithHook();
    coffeeWithHook.init();
    

    【好莱坞原则】

      下面引入一个新的设计原则——“好莱坞原则”。好莱坞无疑是演员的天堂,但好莱坞也有很多找不到工作的新人演员,许多新人演员在好莱坞把简历递给演艺公司之后就只有回家等待电话。有时候该演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答:“不要来找我,我会给你打电话。”

      在设计中,这样的规则就称为好莱坞原则。在这一原则的指导下,允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”

      模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。除此之外,好莱坞原则还常常应用于其他模式和场景,例如发布——订阅模式和回调函数

      在发布—订阅模式中,发布者会把消息推送给订阅者,这取代了原先不断去fetch消息的形式。例如假设乘坐出租车去一个不了解的地方,除了每过5秒钟就问司机“是否到达目的地”之外,还可以在车上美美地睡上一觉,然后跟司机说好,等目的地到了就叫醒你。这也相当于好莱坞原则中提到的“别调用我们,我们会调用你”

      在ajax异步请求中,由于不知道请求返回的具体时间,而通过轮询去判断是否返回数据,这显然是不理智的行为。所以通常会把接下来的操作放在回调函数中,传入发起ajax异步请求的函数。当数据返回之后,这个回调函数才被执行,这也是好莱坞原则的一种体现。把需要执行的操作封装在回调函数里,然后把主动权交给另外一个函数。至于回调函数什么时候被执行,则是另外一个函数控制的

    【基于继承】

      模板方法模式是基于继承的一种设计模式,父类封装了子类的算法框架和方法的执行顺序,子类继承父类之后,父类通知子类执行这些方法,好莱坞原则很好地诠释了这种设计技巧,即高层组件调用底层组件

      模板方法模式是为数不多的基于继承的设计模式,但javascript语言实际上没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的。也就是说,虽然在形式上借鉴了提供类式继承的语言,但本文的模板方法模式并不十分正宗。而且在javascript这般灵活的语言中,实现这样一个例子,是否真的需要继承这种重武器呢?在好莱坞原则的指导之下,下面这段代码可以达到和继承一样的效果

    var Beverage = 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 = Beverage({
        brew: function(){
            console.log( '用沸水冲泡咖啡' );
        },
        pourInCup: function(){
            console.log( '把咖啡倒进杯子' );
        },
        addCondiments: function(){
            console.log( '加糖和牛奶' );
        }
    });
    
    var Tea = Beverage({
        brew: function(){
            console.log( '用沸水浸泡茶叶' );
        },
        pourInCup: function(){
            console.log( '把茶倒进杯子' );
        },
        addCondiments: function(){
            console.log( '加柠檬' );
        }
    });
    var coffee = new Coffee();
    coffee.init();
    var tea = new Tea();
    tea.init();
    

      在这段代码中,把brew、pourInCup、addCondiments这些方法依次传入Beverage函数,Beverage函数被调用之后返回构造器F。F类中包含了“模板方法”F.prototype.init。跟继承得到的效果一样,该“模板方法”里依然封装了饮料子类的算法框架

      模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放——封闭原则的。但在javascript中,很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择

     

    钩子方法

      通过模板方法模式,在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?比如在饮料类Beverage中封装了饮料的冲泡顺序:

      1、把水煮沸

      2、用沸水冲泡饮料

      3、把饮料倒进杯子

      4、加调料

      这4个冲泡饮料的步骤适用于咖啡和茶,在饮料店里,根据这4个步骤制作出来的咖啡和茶,一直顺利地提供给绝大部分客人享用。但有一些客人喝咖啡是不加调料(糖和牛奶)的。既然Beverage作为父类,已经规定好了冲泡饮料的4个步骤,那么有什么办法可以让子类不受这个约束呢?

      钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能

      在下面这个例子里,把挂钩的名字定为customerWantsCondiments,接下来将挂钩放入Beverage类,看看如何得到一杯不需要糖和牛奶的咖啡,代码如下:

    var Beverage = function(){};
    Beverage.prototype.boilWater = function(){
        console.log( '把水煮沸' );
    };
    Beverage.prototype.brew = function(){
        throw new Error( '子类必须重写brew 方法' );
    };
    Beverage.prototype.pourInCup = function(){
        throw new Error( '子类必须重写pourInCup 方法' );
    };
    Beverage.prototype.addCondiments = function(){
        throw new Error( '子类必须重写addCondiments 方法' );
    };
    Beverage.prototype.customerWantsCondiments = function(){
        return true; // 默认需要调料
    };
    Beverage.prototype.init = function(){
        this.boilWater();
        this.brew();
        this.pourInCup();
        if ( this.customerWantsCondiments() ){ // 如果挂钩返回true,则需要调料
            this.addCondiments();
        }
    };
    
    var CoffeeWithHook = function(){};
    CoffeeWithHook.prototype = new Beverage();
    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 coffeeWithHook = new CoffeeWithHook();
    coffeeWithHook.init();
    

    【好莱坞原则】

      下面引入一个新的设计原则——“好莱坞原则”。好莱坞无疑是演员的天堂,但好莱坞也有很多找不到工作的新人演员,许多新人演员在好莱坞把简历递给演艺公司之后就只有回家等待电话。有时候该演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答:“不要来找我,我会给你打电话。”

      在设计中,这样的规则就称为好莱坞原则。在这一原则的指导下,允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”

      模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。除此之外,好莱坞原则还常常应用于其他模式和场景,例如发布——订阅模式和回调函数

      在发布—订阅模式中,发布者会把消息推送给订阅者,这取代了原先不断去fetch消息的形式。例如假设乘坐出租车去一个不了解的地方,除了每过5秒钟就问司机“是否到达目的地”之外,还可以在车上美美地睡上一觉,然后跟司机说好,等目的地到了就叫醒你。这也相当于好莱坞原则中提到的“别调用我们,我们会调用你”

      在ajax异步请求中,由于不知道请求返回的具体时间,而通过轮询去判断是否返回数据,这显然是不理智的行为。所以通常会把接下来的操作放在回调函数中,传入发起ajax异步请求的函数。当数据返回之后,这个回调函数才被执行,这也是好莱坞原则的一种体现。把需要执行的操作封装在回调函数里,然后把主动权交给另外一个函数。至于回调函数什么时候被执行,则是另外一个函数控制的

    【基于继承】

      模板方法模式是基于继承的一种设计模式,父类封装了子类的算法框架和方法的执行顺序,子类继承父类之后,父类通知子类执行这些方法,好莱坞原则很好地诠释了这种设计技巧,即高层组件调用底层组件

      模板方法模式是为数不多的基于继承的设计模式,但javascript语言实际上没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的。也就是说,虽然在形式上借鉴了提供类式继承的语言,但本文的模板方法模式并不十分正宗。而且在javascript这般灵活的语言中,实现这样一个例子,是否真的需要继承这种重武器呢?在好莱坞原则的指导之下,下面这段代码可以达到和继承一样的效果

    var Beverage = 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 = Beverage({
        brew: function(){
            console.log( '用沸水冲泡咖啡' );
        },
        pourInCup: function(){
            console.log( '把咖啡倒进杯子' );
        },
        addCondiments: function(){
            console.log( '加糖和牛奶' );
        }
    });
    
    var Tea = Beverage({
        brew: function(){
            console.log( '用沸水浸泡茶叶' );
        },
        pourInCup: function(){
            console.log( '把茶倒进杯子' );
        },
        addCondiments: function(){
            console.log( '加柠檬' );
        }
    });
    var coffee = new Coffee();
    coffee.init();
    var tea = new Tea();
    tea.init();
    

      在这段代码中,把brew、pourInCup、addCondiments这些方法依次传入Beverage函数,Beverage函数被调用之后返回构造器F。F类中包含了“模板方法”F.prototype.init。跟继承得到的效果一样,该“模板方法”里依然封装了饮料子类的算法框架

      模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放——封闭原则的。但在javascript中,很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择

     

    模板方法模式定义

      模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。

      模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

      模板方法非常常见,对创建框架来说,由框架控制如何做事情,而由你(使用这个框架的人)指定框架算法中每个步骤的细节。(可以想想单元测试的框架JUnit的实现。)

     

    捋一遍整个流程

    1. 我需要做一个茶:

    新葡亰496net 15

    1. 然后调用茶的模板方法:

    新葡亰496net 16

    1. 在模板方法里面执行下列工序:

    boildWater();

    brew();

    pourInCup();

    addCondiments();

     

    新葡亰496net:之模板方法格局,读书笔记09。对模板方法进行挂钩

      钩子(hook)是一种被声明在抽象类中的方法,但只有空的或者默认的实现

      钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

      某些步骤是可选的,所以可以将这些步骤实现成钩子,而不是实现成抽象方法,这样就可以让抽象类的子类的负荷减轻。

      比如,也可以利用钩子做条件控制,影响抽象类中的算法流程:钩子方法在抽象类中有默认实现返回true,放在抽象类的if条件语句中,子类可以覆盖也可以不覆盖这个钩子方法。

     

    模板方法有什么好处?

    不使用模板方法时:

    • 咖啡和茶各自控制自己的算法.
    • 饮料间的代码重复.
    • 改变算法需要修改多个地方
    • 添加新饮料需要做很多工作.
    • 算法分布在了不同的类里面

    使用模板方法后:

    • CaffeineBeverage这个父类控制并保护算法
    • 父类最大化的代码的复用
    • 算法只在一个地方, 改变算法也只需改变这个地方
    • 新的饮料只需实现部分工序即可
    • 父类掌握着算法, 但是依靠子类去做具体的实现.

    用模板方法排序

      Java数组类的设计者提供给我们一个方便的模板方法用来排序。必须实现Comparable接口,提供这个接口所声明的compareTo()方法。

    好莱坞原则

      好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。

      好莱坞原则可以给我们一种防止“依赖腐败”的方法。

      当组件之间太多依赖的时候,设计就变得难懂了。

     

      在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。

      换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。

    模板方法定义

    模板方法在一个方法里定义了一套算法的骨架, 算法的某些步骤可以让子类来实现. 模板方法让子类重新定义算法的某些步骤而无需改变算法的结构.

    类图:

    新葡亰496net 17

    这个抽象类:

    新葡亰496net 18

    针对这个抽象类, 我们可以有一些扩展:

    新葡亰496net 19

    看这个hook方法, 它是一个具体的方法, 但是啥也没做, 这种就叫做钩子方法. 子类可以重写该方法, 也可以不重写.

    其他模板方法实例

    1. java.io的InputStream类有一个read()方法,是由子类实现的,而这个方法又会被read(byte b[], int off, int len)模板方法使用。
    2. Swing的JFrame继承了一个paint()方法。在默认状态下,paint()是不做事情的,因为它是一个“钩子”。通过覆盖paint(),可以将自己的代码插入JFrame的算法中,显示出想要的画面。
    3. applet是一个能在网页上面执行的小程序。任何applet必须继承自Applet类,而Applet类中提供了好些钩子。

     

     

    模板方法里面的钩子

    所谓的钩子, 它是一个在抽象类里面声明的方法, 但是方法里面默认的实现是空的. 这也就给了子类"钩进"算法某个点的能力, 当然子类也可以不这么做, 就看子类是否需要了.

    看这个带钩子的饮料父类:

    新葡亰496net 20

    customerWantsCondiments()就是钩子, 子类可以重写它.

    在prepareRecipe()方法里面, 通过这个钩子方法的结果来决定是否添加调料.

    下面是使用这个钩子的咖啡:

    新葡亰496net 21

    好莱坞原则和依赖倒置原则

      依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。

      而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖底层组件。

      两者的目标都是在于解耦

     

    C#代码实现

    模板方法模式和其他模式

      策略模式和模板方法模式都封装算法,一个用组合,一个用继承。

      工厂方法是模板方法的一种特殊版本。

    不带钩子的父类:

    using System;
    
    namespace TemplateMethodPattern.Abstractions
    {
        public abstract class CaffeineBeverage
        {
            public void PrepareRecipe()
            {
                BoilWater();
                Brew();
                PourInCup();
                AddCondiments();
            }
    
            protected void BoilWater()
            {
                Console.WriteLine("Boiling water");
            }
    
            protected abstract void Brew();
    
            protected void PourInCup()
            {
                Console.WriteLine("Pouring into cup");
            }
    
            protected abstract void AddCondiments();
        }
    }
    

    咖啡和茶:

    using System;
    using TemplateMethodPattern.Abstractions;
    
    namespace TemplateMethodPattern.Beverages
    {
        public class Coffee: CaffeineBeverage
        {
            protected override void Brew()
            {
                Console.WriteLine("Dripping Coffee through filter");
            }
    
            protected override void AddCondiments()
            {
                Console.WriteLine("Adding Sugar and Milk");
            }
        }
    }
    
    using System;
    using TemplateMethodPattern.Abstractions;
    
    namespace TemplateMethodPattern.Beverages
    {
        public class Tea: CaffeineBeverage
        {
            protected override void Brew()
            {
                Console.WriteLine("Steeping the tea");
            }
    
            protected override void AddCondiments()
            {
                Console.WriteLine("Adding Lemon");
            }
        }
    }
    

    测试:

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

    新葡亰496net 22

     

    带钩子的父类:

    using System;
    
    namespace TemplateMethodPattern.Abstractions
    {
        public abstract class CaffeineBeverageWithHook
        {
            public void PrepareRecipe()
            {
                BoilWater();
                Brew();
                PourInCup();
                if (CustomerWantsCondiments())
                {
                    AddCondiments();
                }
            }
    
            protected abstract void Brew();
            protected abstract void AddCondiments();
    
            protected void BoilWater()
            {
                Console.WriteLine("Boiling water");
            }
    
            protected void PourInCup()
            {
                Console.WriteLine("Pouring into cup");
            }
    
            public virtual bool CustomerWantsCondiments()
            {
                return true;
            }
        }
    }
    

    咖啡:

    using System;
    using TemplateMethodPattern.Abstractions;
    
    namespace TemplateMethodPattern.Beverages
    {
        public class CoffeeWithHook: CaffeineBeverageWithHook
        {
            protected override void Brew()
            {
                Console.WriteLine("Dripping Coffee through filter");
            }
    
            protected override void AddCondiments()
            {
                Console.WriteLine("Adding Sugar and Milk");
            }
    
            public override bool CustomerWantsCondiments()
            {
                var answer = GetUserInput();
                if (answer == "yes")
                {
                    return true;
                }
                return false;
            }
    
            private string GetUserInput()
            {
                Console.WriteLine("Would you like milk and sugar with you coffee (y/n) ?");
                var keyInfo = Console.ReadKey();
                return keyInfo.KeyChar == 'y' ? "yes" : "no";
            }
        }
    }
    

    新葡亰496net,测试:

            static void MakeCoffeeWithHook()
            {
                var coffeeWithHook = new CoffeeWithHook();
                Console.WriteLine("Making coffee...");
                coffeeWithHook.PrepareRecipe();
            }
    

    新葡亰496net 23

    钩子和抽象方法的区别?

    抽象方法是算法里面必须要实现的一个方法或步骤, 而钩子是可选实现的.

     

    好莱坞设计原则

    好莱坞设计原则就是: 别给我们打电话, 我们会给你打电话.

    好莱坞原则可以防止依赖关系腐烂. 依赖关系腐烂是指高级别的组件依赖于低级别的组件, 它又依赖于高级别组件, 它又依赖于横向组件, 又依赖于低级别组件....以此类推. 当腐烂发生的时候, 没人会看懂你的系统是怎么设计的.

    而使用好莱坞原则, 我们可以让低级别组件钩进一个系统, 但是高级别组件决定何时并且以哪种方式它们才会被需要. 换句话说就是, 高级别组件对低级别组件说: "别给我们打电话, 我们给你们打电话".

    新葡亰496net 24

    好莱坞原则和模板方法模式

    新葡亰496net 25

    模板方法里, 父类控制算法, 并在需要的时候调用子类的方法.

    而子类从来不会直接主动调用父类的方法.

    其他问题

    好莱坞原则和依赖反转原则DIP的的区别?

    DIP告诉我们不要使用具体的类, 尽量使用抽象类. 而好莱坞原则则是让低级别组件可以被钩进算法中去, 也没有建立低级别组件和高级别组件间的依赖关系.

    三种模式比较:

    模板方法模式: 子类决定如何实现算法中特定的步骤

    策略模式: 封装变化的行为并使用委托来决定哪个行为被使用.

    工厂方法模式: 子类决定实例化哪个具体的类.

    使用模板方法做排序

    看看java里面数组的排序方法:

    新葡亰496net 26

    新葡亰496net 27

    mergeSort就可以看做事模板方法, compareTo()就是需要具体实现的方法.

    但是这个并没有使用子类, 但是根据实际情况, 还是可以灵活使用的, 你需要做的就是实现Comparable接口即可., 这个接口里面只有一个CompareTo()方法.

    具体使用C#就是这样:

    鸭子:

    using System;
    
    namespace TemplateMethodPattern.ForArraySort
    {
        public class Duck : IComparable
        {
            private readonly string _name;
            private readonly int _weight;
    
            public Duck(string name, int weight)
            {
                _name = name;
                _weight = weight;
            }
    
            public override string ToString()
            {
                return $"{_name} weights {_weight}";
            }
    
            public int CompareTo(object obj)
            {
                if (obj is Duck otherDuck)
                {
                    if (_weight < otherDuck._weight)
                    {
                        return -1;
                    }
                    if (_weight == otherDuck._weight)
                    {
                        return 0;
                    }
                }
                return 1;
            }
        }
    }
    

    比较鸭子:

            static void SortDuck()
            {
                var ducks = new Duck[]
                {
                    new Duck("Duffy", 8),
                    new Duck("Dewey",  2),
                    new Duck("Howard", 7),
                    new Duck("Louie", 2),
                    new Duck("Donal", 10),
                    new Duck("Huey", 3)
                };
                Console.WriteLine("Before sorting:");
                DisplayDucks(ducks);
    
                Array.Sort(ducks);
    
                Console.WriteLine();
                Console.WriteLine("After sorting:");
                DisplayDucks(ducks);
            }
    
            private static void DisplayDucks(Duck[] ducks)
            {
                foreach (Duck t in ducks)
                {
                    Console.WriteLine(t);
                }
            }
    

    新葡亰496net:之模板方法格局,读书笔记09。效果:

    新葡亰496net 28

    其他钩子例子

    java的JFrame:

    新葡亰496net 29

    JFrame父类里面有一个update()方法, 它控制着算法, 我们可以使用paint()方法来钩进到该算法的那部分.

    父类里面JFrame的paint()啥也没做, 就是个钩子, 我们可以在子类里面重写paint(), 上面例子的效果就是:

    新葡亰496net 30

     

    另一个例子Applet小程序:

     新葡亰496net 31

    这5个方法全是重写的钩子...

    我没看过winform或者wpf/sl的源码, 我估计也应该有一些钩子吧.

    总结

    好莱坞原则: "别给我们打电话, 我们给你打电话"

    模板方法模式: 模板方法在一个方法里定义了一套算法的骨架, 算法的某些步骤可以让子类来实现. 模板方法让子类重新定义算法的某些步骤而无需改变算法的结构

    该系列的源码: 

    本文由新葡亰496net发布于奥门新萄京娱乐场,转载请注明出处:新葡亰496net:之模板方法格局,读书笔记09

    关键词: