《Head First设计模式》第八章笔记-模板方法模式

模板方法模式

之前所学习的模式都是围绕着封装进行,如对象创建、方法调用、复杂接口的封装等,这次的模板方法模式将深入封装算法块,好让子类可以在任何时候都将自己挂接进运算里。

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

模板方法就是一个固定步骤的“算法”骨架方法。这个算法的可变部分通过继承,在子类中重载实现。这样就可以在算法骨架不变的情况下,算法细节步骤根据不同的需求进行适应的改变。

例题:茶饮店的饮品冲泡程序(泡茶与泡咖啡)

1

2

3

4

5

6

泡茶:                                             |            咖啡:

                                                  |

1. 煮沸水                                          |              1.煮沸水         

2. 加入茶叶冲泡                                     |              2.加入咖啡粉冲泡

3. 根据需求加入调料(如蜂蜜、柠檬)                   |              3.根据需求加入调料(如牛奶、糖)

4. 将泡好的茶水倒入杯子                              |              4.将泡好的咖啡倒入杯子

我们发现两者的步骤非常相似,仅有部分细节不一:如泡茶冲的是茶叶,加的是蜂蜜;泡咖啡加的是牛奶其实泡茶和泡咖啡的过程就是一个固定骨架步骤的“算法”,我们可以抽象为:

  1. 煮沸水
  2. 冲泡
  3. 根据需求加入调料
  4. 将泡好的饮料倒入杯子

斜体部分为算法中不一样的部分,如何解决?下面,我们用“模板方法模式”来解决这种不一致。

首先,定义一个含有固定骨架“模板方法”的咖啡因饮料抽象类:

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

/**

 *咖啡因饮料

 */

public abstract class CaffeineBeverage {

    /**

     *模板方法,准备饮料

     */

    public final void prepareRecipe(){

        boilWater();

        brew();

        //用于模板方法的算法中可选部分的控制

        if(customerWantsCondiment())

            addCondiment();

        pourInCup();

    }

 

    /**

     *煮沸水

     */

    public void boilWater() {

        System.out.println("煮沸水");

    }

 

    /**

        *冲泡

     */

    public abstract void brew();

 

    /**

     *增加调味剂

     */

    public abstract void addCondiment();

 

    /**

      *将饮料倒入杯子

     */

    public void pourInCup() {

        System.out.println("将饮料倒入杯中");

    }

 

    /**

     *“钩子”方法。顾客决定是否加调料

     */

    public Boolean customerWantsCondiment(){

        return true;

    }

}

这时,准备饮料的四个固定步骤我们都写在模板方法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

public class Tea extends CaffeineBeverage{

    @Override

    public void brew() {

        System.out.println("浸泡茶叶");

    }

 

    @Override

    public void addCondiment() {

        System.out.println("添加蜂蜜");

    }

}

 

public class Coffee extends CaffeineBeverage{

    @Override

    public void brew() {

        System.out.println("冲泡咖啡粉");

    }

 

    @Override

    public void addCondiment() {

        System.out.println("添加糖和牛奶");

    }

}

测试:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public class Main {

    public static void main(String[] args) {

        CaffeineBeverage tea = new Tea();

        tea.prepareRecipe();

        System.out.println("===============");

        CaffeineBeverage coffee = new Coffee();

        coffee.prepareRecipe();

    }

}

/**输出:

 煮沸水

 浸泡茶叶

 添加蜂蜜

 将饮料倒入杯中

 ===============

 煮沸水

 冲泡咖啡粉

 添加糖和牛奶

 将饮料倒入杯中

*/

可以看到,通过继承,模板方法在茶和咖啡中的实现有了差别。
模板方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这样可以确保算法的结构保持不变,同时由子类提供部分的实现。
所以,模板方法就是定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
最后,我们给出模板方法的类图:
模板方法模式类图

看上去模板方法似乎就这样结束了,然而,在上面定义的抽象类中还有一个“钩子(hook)”方法,
什么是“钩子”方法呢?我们来看一下定义:
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

在上面的代码中,我们写了一个钩子来决定是否加调料

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

/**

 * “钩子”方法。顾客决定是否加调料

 */

public Boolean customerWantsCondiment(){

    return true;

}

/**

 * 模板方法,准备饮料

 */

public final void prepareRecipe(){

    boilWater();

    brew();

    //用于模板方法的算法中可选部分的控制

    if(customerWantsCondiment())

        addCondiment();

    pourInCup();

}

现在,我们用一个子类来挂钩:

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

public class Tea extends CaffeineBeverage{

    @Override

    public void brew() {

        System.out.println("浸泡茶叶");

    }

 

    @Override

    public void addCondiment() {

           System.out.println("添加蜂蜜");

    }

 

    //覆盖父类的“钩子”方法,更改算法中的可选部分

    @Override

    public Boolean customerWantsCondiment(){

        //询问顾客是否需要调料

        String answer = askCustomerNeedCondiment();

        if("y".equals(answer))

            return true;

        else

            return false;

    }

 

    private String askCustomerNeedCondiment() {

        String answer = null;

        System.out.println("请问您要不要加蜂蜜?请回答y或n");

        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        try {

            answer = in.readLine();

        catch (IOException e) {

            e.printStackTrace();

        }

        return answer;

    }

}

这时,我们准备茶水时就能根据顾客的回答而安排需要加调料这一步骤了。子类通过覆盖钩子方法,实现了算法中的可选部分。

模板方法模式中还使用到了一个新的设计原则:好莱坞原则
好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你

好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。

好莱坞原则就是确保不会出现高层组件依赖底层组件、底层组件又依赖高层组件的“依赖腐败”。只有高层组件会决定什么时候和怎样使用底层组件,而底层组件不会调用高层组件

好莱坞原则
在模板方法模式中,算法的实现会调用到具体子类的某个方法,也就是高层组件依赖于底层组件。具体子类不会调用父类中的方法,不会形成底层组件依赖高层组件的环状依赖:

模板方法模式依赖

采用好莱坞原则的设计模式还有:工厂方法(可以看作特殊的模板方法),观察者、装饰者...

好莱坞原则和依赖倒置原则的关系:依赖倒置原则是尽量避免使用具体类,多使用抽象。

策略模式与模板方法模式

策略模式和模板方法模式很像,都是针对算法改变的情况的设计模式。以下是它们的区别:

  • 策略模式是采用的组合来实现算法的变化,这样的设计更加灵活,依赖性程度低;
  • 模板方法模式采用的继承来实现算法中的变化部分,这样的设计对算法有更多的控制权,且代码的重复会少一些,但由于算法依赖于父类,所以依赖程度高。

Java API中的模板方法

Java中较常见的模板方法模式的应用:

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

注意,这个排序的例子表面看上去好像与模板方法模式无关(没有用到继承),但实质仍是通过子类提供算法步骤的实现来实现了算法的变化。虽然采用了组合,但思想仍是模板方法模式的思想。

总结

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

  • 模板方法定义了算法的步骤,把这些步骤实现延迟到子类。
  • 模板方法为我们提供了一种代码复用的重要技巧。
  • 模板方法的抽象类可以定义具体的方法、抽象方法和钩子方法。
  • 抽象方法由子类实现。
  • 钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要覆盖它。
  • 好莱坞原则告诉我们将决策权放在高层模板中,以便决定如何及何时调用底层模块。
  • 策略模式和模板方法模式都封装算法,一个是用组合,一个是用继承。
  • 工厂方法是模板方法的一种特殊版本

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/445419.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习基础-吴恩达-coursera-(第一周学习笔记)----Introduction and Linear Regression

课程网址:https://www.coursera.org/learn/machine-learning Week 1 —— Introduction and Linear Regression 目录 Week 1 Introduction and Linear Regression目录一 介绍1-1 机器学习概念及应用1-2 机器学习分类 二 单变量的线性回归2-1 假设函数hypothesis2…

常见8种机器学习算法总结

简介 机器学习算法太多了,分类、回归、聚类、推荐、图像识别领域等等,要想找到一个合适算法真的不容易,所以在实际应用中,我们一般都是采用启发式学习方式来实验。通常最开始我们都会选择大家普遍认同的算法,诸如SVM&a…

redis——数据结构(字典、链表、字符串)

1 字符串 redis并未使用传统的c语言字符串表示,它自己构建了一种简单的动态字符串抽象类型。 在redis里,c语言字符串只会作为字符串字面量出现,用在无需修改的地方。 当需要一个可以被修改的字符串时,redis就会使用自己实现的S…

Hotspot虚拟机的对象

创建 Step1:类加载检查 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 Step2:分…

redis——数据结构(整数集合,压缩列表)

4、整数集合 整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 可以保存 int16_t 、 int32_t 、 int64_t 的整数值, 并且保证集合中不会出现重复元素。 实现较为简单: typedef struct intset {// 编码方…

机器学习知识总结系列- 知识图谱(0-0)

文章目录目录机器学习知识图谱目录 本系列的文章只是根据个人的习惯进行总结,可能结构与一些书籍上不太一样,开始的内容比较简单,会随着后续的深入,不断丰富和更新图谱,同时也期待有相同兴趣的朋友一起给我留言一起丰富…

跳表介绍和实现

想慢慢的给大家自然的引入跳表。 想想,我们 1)在有序数列里搜索一个数 2)或者把一个数插入到正确的位置 都怎么做? 很简单吧 对于第一个操作,我们可以一个一个比较,在数组中我们可以二分,这…

机器学习知识总结系列- 基本概念(1-0)

文章目录目录1. 机器学习的定义2. 机器学习的分类2.1根据是否在人类监督下进行训练监督学习非监督学习半监督学习强化学习2.2根据是否可以动态渐进的学习在线学习批量学习2.3根据是否在训练数据过程中进行模式识别实例学习基于模型的学习3. 机器学习中的一些常见名词4. 机器学习…

剑指offer(刷题21-30)--c++,Python版本

文章目录目录第 21题:解题思路:代码实现:cpython第22 题:解题思路:代码实现:cpython第23 题:解题思路:代码实现:cpython第24 题:解题思路:代码实现…

剑指offer(刷题41-50)--c++,Python版本

文章目录目录第41题:解题思路:代码实现:cpython第42题:解题思路:代码实现:cpython第43题:解题思路:代码实现:cpython第44题:解题思路:代码实现&am…

redis——持久化

因为redis是内存数据库,他把数据都存在内存里,所以要想办法实现持久化功能。 RDB RDB持久化可以手动执行,也可以配置定期执行,可以把某个时间的数据状态保存到RDB文件中,反之,我们可以用RDB文件还原数据库…

剑指offer(刷题51-60)--c++,Python版本

文章目录目录第51题:解题思路:代码实现:cpython第52题:解题思路:代码实现:cpython第53题:解题思路:代码实现:cpython第54题:解题思路:代码实现&am…

2017第一届河北省大学生程序设计竞赛题解

超级密码 小明今年9岁了,最近迷上了设计密码!今天,他又设计了一套他认为很复杂的密码,并且称之为“超级密码”. 说实话,这套所谓的“超级密码”其实并不难:对于一个给定的字符串,你只要提取其中…

大数的四则运算(加法、减法、乘法、除法)

大数的四则运算(加法、减法、乘法、除法) 前言: 在计算机中数字表示的范围是有限制的,比如我们熟知的 int、float、double 等数据类型所能表示的范围都是有限的,如果我们要对位数达到几十位、几百位、上千位的大整数进…

随机过程1

随机过程1概述1.参考书目2.主要内容3.概率论--基本概念回顾3.1对“不确定性”的认识3.2 应对“不确定性”应该怎么做3.3随机变量(Random Variable)3.4分布函数(Distribution Function)3.5概率密度(Density)…

数组基操三连(4)

题目一 给定一个长度为N的整型数组arr,其中有N个互不相等的自然数1~N 请实现arr的排序 但是不要把下标0~N-1位置上的数值通过直接赋值的方式替换成1~N。 要求:时间复杂度为O(N),额外空间复杂度为O(1)。 思路:从左向右检查&…

Linux(1)-touch,mkdir,rm,mv,cp,ls,cd,cat

Linux1-实用终端命令1. touch, mkdir2. rm, mv, cp3. ls(通配符),cd(绝对/相对路径)4. cat, more/less文件内容浏览文件/目录-增删查改, 文件内容查看.1. touch, mkdir touch新文件 :在当前文件夹下,创建文件。文件不存在则创建新文件;文件存…