《Head first设计模式》之模版方法模式
模板方法模式在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
有些人没有咖啡就活不下去;有些人则离不开茶。两者共同的成分是什么?当然是咖啡因了!
但还不只这样。茶和咖啡的冲泡方式非常相似:
星巴兹咖啡冲泡法
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
星巴兹茶冲泡法
- 把水煮沸
- 用沸水冲泡茶叶
- 把茶倒进杯子
- 加柠檬
下面我们用代码来创建咖啡和茶:
// 这是我们的咖啡类,用来煮咖啡
public class Coffee {
// 这是我们的咖啡冲泡法
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugerAndMilk();
}
// 煮沸水
private void boilWater() {
System.out.println("Boiling water");
}
// 冲泡咖啡
private void brewCoffeeGrinds() {
System.out.println("Dripping coffee through filter");
}
// 把咖啡倒进杯子
private void pourInCup() {
System.out.println("Pouring into cup");
}
// 加糖和奶
private void addSugerAndMilk() {
System.out.println("Adding Sugar and Milk");
}
}
// 这是我们的茶类,用来煮茶
public class Tea {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
// 煮沸水。这个方法和咖啡类完全一样
private void boilWater() {
System.out.println("Boiling water");
}
// 冲泡茶叶
private void steepTeaBag() {
System.out.println("Steeping the tea");
}
// 把茶倒进杯子。这个方法和咖啡类完全一样
private void pourInCup() {
System.out.println("Pouring into cup");
}
// 加柠檬
private void addLemon() {
System.out.println("Adding Lemon");
}
}
我们发现了重复的代码,这表示我们需要清理一下设计了。在这里,茶和咖啡是如此得相似,似乎我们应该将共同的部分抽取出来,放进一个基类中。
第一版设计
看起来这个咖啡和茶的设计相当简单,你的第一版设计,可能看起来像这样:
public abstract class CaffeineBeverage {
// prepareRecipe()方法在每个类中都不一样,所以定义成抽象方法。
abstract void prepareRecipe();
// 以下两个方法被两个子类所共享,所以被定义在这个超类中
public void boilWater() {
System.out.println("Boiling water");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
}
public class Coffee extends CaffeineBeverage {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugerAndMilk();
}
private void brewCoffeeGrinds() {
System.out.println("Dripping coffee through filter");
}
private void addSugerAndMilk() {
System.out.println("Adding Sugar and Milk");
}
}
public class Tea extends CaffeineBeverage {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
private void steepTeaBag() {
System.out.println("Steeping the tea");
}
private void addLemon() {
System.out.println("Adding Lemon");
}
}
更进一步的设计
以上的设计是不是忽略了某些其他的共同点?咖啡和茶之间还有什么是相似的?
注意到两份冲泡法都采用了相同的算法:
- 把水煮沸
- 用热水泡咖啡或茶
- 把饮料倒进杯子
- 在饮料中加入适当的调料
其中,第2步和第4步并没有被抽取出来,但他们是一样的,只是应用在了不同的饮料上。我们有办法把prepareRecipe()方法也抽象化吗?让我们先从每一个子类中逐步抽象prepareRecipe()。
抽象prepareRecipe()
我们遇到的第一个问题就是,咖啡使用brewCoffeeGrinds()和addSugerAndMilk()方法,而茶使用steepTeaBag()和addLemon()方法。让我们思考这一点:浸泡(steep)和冲泡(brew)差异其实不大。所以我们给它一个新的方法名称,比方说brew(),然后不管是泡茶或者冲泡咖啡我们都用这个名称。类似地,加糖和牛奶都是在饮料中加入调料。让我们也给它一个新的方法名称:addCondiments()。这样一来,新的prepareRecipe()方法看起来像是这样:
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
现在我们有了新的prepareRecipe()方法,但是需要让它能够符合代码。要想这么做,我们先从CaffeineBeverage超类开始:
public abstract class CaffeineBeverage {
// 现在,用同一个prepareRecipe()方法来处理茶和咖啡。
// prepareRecipe()方法被声明为final,因为我们不希望子类覆盖这个方法
// 我们将第2步和第4步泛化成为brew()和addCondiments()
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
// 因为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,
// 剩余的东西留给子类去操心
abstract void addCondiments();
abstract void brew();
public void boilWater() {
System.out.println("Boiling water");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
}
最后,我们需要处理咖啡和茶类,这两个类现在都是依赖超类来处理冲泡法,所以只需要自行处理冲泡和添加调料部分:
public class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Dripping coffee through filter");
}
@Override
void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
}
public class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Steeping the tea");
}
@Override
void addCondiments() {
System.out.println("Adding Lemon");
}
}
认识模板方法
基本上,我们刚刚实现的就是模板方法模式。咖啡因饮料类的结构包含了实际的“模板方法”:prepareRecipe()方法。为什么? 因为:
- 毕竟它是一个方法。
- 它用作一个算法的模板,在这个例子中,算法是用来制作咖啡因饮料的。
在这个模板中,算法内的每一个步骤都被一个方法代表了。某些方法是由这个类(也就是超类)处理的,某些方法则是由子类处理的。需要由子类提供的方法,必须在超类中声明为抽象。
模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
模板方法带给我们什么?
不好的茶和咖啡实现 | 模板方法提供的酷炫咖啡因饮料 |
---|---|
Coffee和Tea主导一切,它们控制了算法。 | 由CaffeineBeverage类主导一切,它拥有算法,并且保护这个算法。 |
Coffee和Tea之间存在着重复的代码。 | 对子类来说,CaffeineBeverage类的存在,可以将代码的复用最大化。 |
对于算法所做的代码改变,需要打开子类修改许多地方。 | 算法只存在一个地方,所以很容易修改。 |
由于类的组织方式不具有弹性,所以加入新种类的咖啡因饮料需要做许多工作。 | 这个模板方法提供了一个框架,可以让其他的咖啡因饮料插进来。新的咖啡因饮料只需要实现自己的方法就可以了。 |
算法的知识和它的实现会分散在许多类中。 | CaffeineBeverage类专注在算法本身,而由子类提供完整的实现。 |
定义模板方法模式
你已经看到了茶和咖啡的例子中如何使用模板方法模式。现在,就让我们来看看这个模式的正式定义和所有的细节:
模板方法模式在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这个模式是用来创建一个算法的模板。什么是模板?如你所见的,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
让我们细看抽象类是如何被定义的,包括了它内含的模板方法和原语操作。
// 这就是我们的抽象类。它被声明为抽象,用来作为基类,其子类必须实现其操作
public abstract class AbstractClass {
// 这就是模板方法。它被声明为final,以免子类改变这个算法的顺序。
final void templateMethod() {
// 模板方法定义了一连串的步骤,每个步骤由一个方法代表
primitiveOperation1();
primitiveOperation2();
concreteOperation();
}
// 在这个范例中有两个原语操作,具体子类必须实现它们
abstract void primitiveOperation1();
abstract void primitiveOperation2();
// 这个抽象类有一个具体的操作。
void concreteOperation() {
// ...
}
}
现在我们要“更靠近一点”,详细看看此抽象类内可以有哪些类型的方法:
public abstract class AbstractClass {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
// 我们加进一个新方法调用
hook();
}
// 这两个方法还是和以前一样,定义成抽象,由具体的子类实现。
abstract void primitiveOperation1();
abstract void primitiveOperation2();
// 这个具体的方法被定义在抽象类中。
// 将它声明为final,这样一来子类就无法覆盖它。
// 它可以被模板方法直接使用,或者被子类使用。
final void concreteOperation() {
// ...
}
// 我们也可以有“默认不做事的方法”,我们称这种方法为“hook”(钩子)。
// 子类可以视情况决定要不要覆盖它们。在下面,我们就会知道钩子的实际用途
void hook() {}
}
对模板方法进行挂钩
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类决定。
钩子有好几种用途,让我们先看其中一个,稍后再看其他几个:
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 我们加上了一个小小的条件语句,而该条件是否成立,
// 是由一个具体方法customerWantsCondiments()决定的。
// 如果顾客“想要”调料,只有这时我们才调用addCondiments()。
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void addCondiments();
abstract void brew();
public void boilWater() {
System.out.println("Boiling water");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
// 我们在这里定义了一个方法,(通常)是空的缺省实现。这个方法只会返回true,不做别的事。
// 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做。
boolean customerWantsCondiments() {
return true;
}
}
钩子有几种用法。钩子可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对此钩子置之不理。钩子的另一个用法,是让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应。比方说,名为justReOrderList()的钩子方法允许子类在内部列表重新组织后执行某些动作(例如在屏幕上重新显示数据)。正如你刚刚看到的,钩子也可以让子类有能力为其抽象类做一些决定。
好莱坞原则
我们有一个新的设计原则,称为好莱坞原则:
好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。
很容易记吧,但这和OO设计又有什么关系呢?
好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。
好莱坞原则和模板方法之间的连接其实还算明显:当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。怎样才能办到呢?让我们再看一次咖啡因饮料的设计:
- CaffeineBeverage是我们的高层组件,它能够控制冲泡法的算法,只有在需要子类实现某个方法时,才调用子类。
- 饮料的客户代码只依赖CaffeineBeverage抽象,而不依赖具体的Tea或Coffee,这可以减少整个系统的依赖。
- Tea和Coffee子类只简单提供brew()和addCondiments()方法的实现细节。如果Tea和Coffee没有先被调用,绝对不会直接调用抽象类。
《Head first设计模式》之模版方法模式的更多相关文章
- JS常用的设计模式(10)——模版方法模式
模式方法是预先定义一组算法,先把算法的不变部分抽象到父类,再将另外一些可变的步骤延迟到子类去实现.听起来有点像工厂模式( 非前面说过的简单工厂模式 ). 最大的区别是,工厂模式的意图是根据子类的实现最 ...
- 设计模式 笔记 模版方法模式 Template Method
//---------------------------15/04/28---------------------------- //TemplateMethod 模版方法模式----类行为型模式 ...
- 设计模式之模版方法模式(Template Method Pattern)
一.什么是模版方法模式? 首先,模版方法模式是用来封装算法骨架的,也就是算法流程 既然被称为模版,那么它肯定允许扩展类套用这个模版,为了应对变化,那么它也一定允许扩展类做一些改变 事实就是这样,模版方 ...
- JAVA设计模式之模版方法模式
在阎宏博士的<JAVA与模式>一书中开头是这样描述模板方法(Template Method)模式的: 模板方法模式是类的行为模式.准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式 ...
- java设计模式之模版方法模式以及在java中作用
模板方法模式是类的行为模式.准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑.不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有 ...
- [Head First设计模式]云南米线馆中的设计模式——模版方法模式
系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 [Head First设计模式]山西面馆中的设计模式——观察者模式 [Head First设计模式]山西面馆中的设计模式— ...
- NET设计模式 第二部分 行为型模式(15):模版方法模式(Template Method)
摘要:Template Method模式是比较简单的设计模式之一,但它却是代码复用的一项基本的技术,在类库中尤其重要. 主要内容 1.概述 2.Template Method解说 3..NET中的Te ...
- 设计模式——模版方法模式详解(论沉迷LOL对学生的危害)
. 实例介绍 在本例中,我们使用一个常见的场景,我们每个人都上了很多年学,中学大学硕士,有的人天生就是个天才,中学毕业就会微积分,因此得了诺贝尔数学奖:也有的人在大学里学了很多东西,过得很充实很满意 ...
- Head First 设计模式笔记(模版方法模式)
1.定义: 在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中.模版方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤. 2.类图: 3.说明: 模版方法可以理解为一个方法里面包 ...
- 【pattern】设计模式(2) - 模版方法模式
前言 一晃一年又过了,还是一样的渣. 一晃2周又过去了,还是没有坚持写博客. 本来前2天说填一下SQL注入攻击的坑,结果看下去发现还是ojdbc.jar中的代码,看不懂啊.这坑暂时填不动,强迫在元旦最 ...
随机推荐
- AVR单片机教程——串口发送
本文隶属于AVR单片机教程系列. 到目前为止,我们的开发板只能处理很小量的数据:读取几个引脚电平,输出几个LED,顶多用数码管显示一个两位数字.至于输入一个指令.输出一条调试信息,甚至用scanf ...
- git 查看修改账号密码
git config user.name 查看用户名 git config user.email 查看用户邮箱 修改用户名和邮箱的命令 git config --glo ...
- Scrapy的基本使用
爬取:http://quotes.toscrape.com 单页面 # -*- coding: utf-8 -*- import scrapy class QuoteSpider(scrapy.Spi ...
- 异数OS TCP协议栈测试(五)--关于QOS与延迟
. 异数OS TCP协议栈测试(五)–关于QOS与延迟 ##本文来自异数OS社区 github: https://github.com/yds086/HereticOS 异数OS社区QQ群: 6524 ...
- Python错误与异常
1 异常和错误 1.1 错误和异常 从软件方面来说,错误是语法或者逻辑上的,语法错误指示软件的结构上有错误,导致不能被解释器解释.当程序的语法正确后,剩下的就是逻辑错误了,逻辑错误可能是由于不完整或者 ...
- python类型-序列
注:本文档主要是学习<Python核心编程(第二版)>时做的资料整理. 1.序列 序列的成员是有序排列的,并且可以通过下标偏移量访问到它的一个或者几个成员,包括字符串(普通字符串和Unic ...
- CQBZOJ 避开怪兽
题目描述 给出一个N行M列的地图,地图形成一个有N*M个格子的矩阵.地图中的空地用'.'表示.其中某些格子有怪兽,用'+'表示.某人要从起点格子'V'走到终点格子'J',他可以向上.下.左.右四个方向 ...
- 龙芯 Fedora 28 设置 VNC
系统为龙芯版Fedora28 (床28) Fedora防火墙默认阻止了VNC所需的端口的访问,导致VNC客户端一直无法连接. 安装VNC Server sudo dnf install tigervn ...
- AI初探
看东西应该记笔记,不然如过眼云烟,如只逛商场,不买东西,不留下带走什么,就是浪费时间,没有收获,仅此开始,定期梳理看过的东西. 人工智能的目的是什么呢? 答:让机器表现得更像人类,甚至在某些技能上超越 ...
- BERT模型总结
BERT模型总结 前言 BERT是在Google论文<BERT: Pre-training of Deep Bidirectional Transformers for Language U ...