设计模式之模板方法

《Head First 设计模式》阅读笔记

Posted by wangtiegang on January 5, 2020

好久没看设计模式了,今天翻开书看了下模板方法模式,看完之后记录下学习笔记。

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

直接看定义,感觉似懂非懂,很难理解其中的要义。模版方法实际上就是将一类算法(此处的算法可以理解为一类业务)的具体步骤抽象出来,将其放到超类中,不变的步骤直接在超类中实现,变化的步骤让子类实现。这样下来有几个好处,第一是子类中不存在重复代码了,第二是算法的步骤已经在超类中定义好,子类无法修改,只能提供某些步骤的具体实现,第三就是设计更有弹性,降低了耦合。

书上的例子

结合书上的具体例子来理解:泡咖啡和泡茶

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

可以看出,泡咖啡和泡茶很相似,它们的过程可以理解为一类算法,1 和 3 是一样的操作,2 和 4 只有细微的差异。如果要用代码实现这两个过程,在使用模版方法之前,可以设想几种方式

  • 直接写两个类,泡茶类和泡咖啡类,互不相干,各自实现步骤

    缺点很明显,重复代码太多,一旦步骤变化,每个类都需要修改

  • 将 1 和 3 步骤定义到超类中,然后定义两个类继承它,子类实现 3 和 4 步骤

    重复的代码抽象到了超类中,避免了重复代码,常规操作,感觉还行,但是还可以提升

  • 将 1 和 3 步骤定义到超类中,将 2 和 4 在超类中实现过程,通过传参数来控制具体逻辑,然后定义两个类继承它,子类传参控制 3 和 4 步骤

    看起来好像代码复用更多了,子类代码很少,但是有个致命缺点,子类数量一多,那么超类中 2 和 4 将有大量条件分支,混乱不利于维护

初步的设想都不是很好,这个时候就轮到模版方法来解决上述问题了。

  • 定义一个超类,将 1 和 3 在超类中实现,2 和 4 定义为抽象方法,强制子类实现,在超类中定义一个控制方法,按顺序调用步骤1234。

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 算法的超类
*/
public abstract class CaffeineBeverage {
    /**
    * 该方法定义了算法的步骤和顺序
    */
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    /**
    * 步骤2 定义为抽象方法,又子类实现
    */
    abstract void brew();

    /**
    * 步骤4 定义为抽象方法,又子类实现
    */
    abstract void addCondiments();

    void boilWater() {
        // 1.把水烧开
    }

    void pourInCup() {
        // 2.把茶或咖啡倒入杯子
    }

}

/**
* 泡茶的类,继承CaffeineBeverage,实现2和4
*/
public class Tea entends CaffeineBeverage {

    @Override
    public void brew() {
        // 2.用热水冲泡茶叶
    }

    @Override
    public void addCondiments() {
        // 4.加柠檬
    }

}

/**
* 泡咖啡的类,继承CaffeineBeverage,实现2和4
*/
public class Coffee entends CaffeineBeverage {

    @Override
    public void brew() {
        // 2.用热水冲泡咖啡
    }

    @Override
    public void addCondiments() {
        // 4.加糖和牛奶
    }

}

// 测试
public class Test {
    public void static main(string[] args) {
        Tea tea = new Tea();
        tea.prepareRecipe();
    }
}

以上就是模版方法的具体实现了,超类定义好了算法的步骤和顺序,实现了通用步骤,而子类只需要实现抽象部分就好了。

有几个细节值得注意下:

  • 超类是抽象的,不允许直接实例化使用,因为超类本身功能是不全的,必须有子类实现缺失部分
  • 由子类实现的部分在超类中是抽象方法
  • 定义算法的步骤和顺序的方法是 final 的,不允许子类改变

钩子方法

钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点就行控制,要不要控制,由子类决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 算法的超类
*/
public abstract class CaffeineBeverage {
    /**
    * 该方法定义了算法的步骤和顺序
    */
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        // 利用钩子控制要不要加料,默认返回true
        if (hook1()){
            addCondiments();
        }
        // 利用钩子添加其他行为
        hook2();
    }

    /**
    * 步骤2 定义为抽象方法,又子类实现
    */
    abstract void brew();

    /**
    * 步骤4 定义为抽象方法,又子类实现
    */
    abstract void addCondiments();

    void boilWater() {
        // 1.把水烧开
    }

    void pourInCup() {
        // 2.把茶或咖啡倒入杯子
    }

    /**
    * 钩子1,默认返回 true,子类可以覆盖
    */
    boolean hook1() {
        return true;
    }

    /**
    * 钩子2,空方法,子类可以覆盖
    */
    void hook2() {

    }
}

当创建一个模版方法时,怎么判断是使用钩子还是抽象步骤方法呢?当算法中的这个步骤是可选的时候,使用钩子,如果这个步骤必须由子类实现,使用抽象方法。具体可以结合实际应用选择。

模版方法在平时工作中很常见,如果运用的好的话,可以提升工作效率,设计出逻辑条理更清晰的代码结构,但是并不是所有的应用都跟书上的例子一样,实际情况往往会出现很多变种,需要理解模版方法的精神去进行辨别。