面向对象设计的设计模式(十三):模板方法模式
在模板方法模式(Template Method Pattern)中,定义一个操作中的算法的框架,而将一些步骤的执行延迟到子类中,使得子类可以在不改变算法的结构的前提下即可重新定义该算法的某些特定步骤。
适用场景
通常一个算法需要几个执行步骤来实现,而有时我们需要定义几种执行步骤一致,但是却可能在某个步骤的实现略有差异的算法。也就是说我们既需要复用实现相同的步骤,也可以通过在某个步骤的不同实现来灵活扩展出更多不同的算法。
在这种场景下,我们可以使用模板方法模式:定义好一个算法的框架,在父类实现可以复用的算法步骤,而将需要扩展和修改其他步骤的任务推迟给子类进行。
现在我们清楚了模板方法模式的适用场景,下面看一下这个模式的成员和类图。
成员与类图
成员
模板方法模式的成员除了客户端以外,只有两个成员:
- 算法类(Algorithm):算法类负责声明算法接口,算法步骤接口。并实现可复用的算法步骤接口,且将需要子类实现的接口暴露出来。
- 具体算法类(Concrete Algorithm):具体算法类负责实现算法类声明的算法步骤接口。
有些参考资料定义这两个成员为
Abstract Class
和Concrete Class
。
下面通过类图来看一下命令模式各个成员之间的关系:
模式类图
模板方法模式类图
由上图可以看出,Algorithm
的excute
方法是算法接口,它在内部调用了三个步骤方法:step1
,step2
,step3
。而step2
是未暴露在外部的,因为这个步骤是需要各个子类复用的。因此Algorithm
只将step1
和step3
暴露了出来以供子类来调用。
代码示例
场景概述
模拟一个制作三种热饮的场景:热美式咖啡,热拿铁,热茶。
场景分析
这三种热饮的制作步骤是一致的,都是三个步骤:
- 步骤一:准备热水
- 步骤二:加入主成分
- 步骤三:加入辅助成分(也可以不加,看具体热饮的种类)
虽然制作步骤是一致的,但是不同种类的热饮在每一步可能是不同的:咖啡和茶叶主成分是咖啡粉和茶叶;而辅助成分:美式咖啡和茶叶可以不添加,而拿铁还需添加牛奶。
而第一步是相同的:准备热水。
根据上面对模板方法模式的介绍,像这样算法步骤相同,算法步骤里的实现可能相同或不同的场景我们可以使用模板方法模式。下面我们看一下如何用代码来模拟该场景。
代码实现
首先我们创建算法类HotDrink
:
//================== HotDrink.h ==================
@interface HotDrink : NSObject
- (void)makingProcess;
- (void)addMainMaterial;
- (void)addIngredients;
@end
//================== HotDrink.m ==================
@implementation HotDrink
- (void)makingProcess{
NSLog(@" ===== Begin to making %@ ===== ", NSStringFromClass([self class]));
[self boilWater];
[self addMainMaterial];
[self addIngredients];
}
- (void)prepareHotWater{
NSLog(@"prepare hot water");
}
- (void)addMainMaterial{
NSLog(@"implemetation by subClasses");
}
- (void)addIngredients{
NSLog(@"implemetation by subClasses");
}
@end
HotDrink
向外部暴露了一个制作过程的接口makingProcess
,这个接口内部调用了热饮的所有制作步骤方法:
- (void)makingProcess{
//准备热水
[self prepareHotWater];
//添加主成分
[self addMainMaterial];
//添加辅助成分
[self addIngredients];
}
HotDrink
只向外暴露了这三个步骤中的两个需要子类按照自己方式实现的接口:
//添加主成分
- (void)addMainMaterial;
//添加辅助成分
- (void)addIngredients;
因为热饮的第一步都是一致的(准备热水),所以第一步骤的接口没有暴露出来给子类实现,而是直接在当前类实现了,这也就是模板方法的一个可以复用代码的优点。
OK,我们现在创建好了算法类,那么根据上面的需求,我们接着创建三个具体算法类:
-
HotDrinkTea
:热茶 -
HotDrinkLatte
:热拿铁 -
HotDrinkAmericano
:热美式
//================== HotDrinkTea.h ==================
@interface HotDrinkTea : HotDrink
@end
//================== HotDrinkTea.m ==================
@implementation HotDrinkTea
- (void)addMainMaterial{
NSLog(@"add tea leaf");
}
- (void)addIngredients{
NSLog(@"add nothing");
}
@end
热茶在addMainMaterial
步骤里面是添加了茶叶,而在addIngredients
步骤没有做任何事情(这里先假定是纯的茶叶)。
类似地,我们看一下两种热咖啡的实现。首先是热拿铁HotDrinkLatte
:
//================== HotDrinkLatte.h ==================
@interface HotDrinkLatte : HotDrink
@end
//================== HotDrinkLatte.m ==================
@implementation HotDrinkLatte
- (void)addMainMaterial{
NSLog(@"add ground coffee");
}
- (void)addIngredients{
NSLog(@"add milk");
}
@end
热拿铁在addMainMaterial
步骤里面是添加了咖啡粉,而在addIngredients
步骤添加了牛奶。
下面再看一下热美式HotDrinkAmericano
:
//================== HotDrinkAmericano.h ==================
@interface HotDrinkAmericano : HotDrink
@end
//================== HotDrinkAmericano.m ==================
@implementation HotDrinkAmericano
- (void)addMainMaterial{
NSLog(@"add ground coffee");
}
- (void)addIngredients{
NSLog(@"add nothing");
}
@end
热美式在addMainMaterial
步骤里面是添加了咖啡粉,而在addIngredients
步骤没有做任何事,因为美式就是纯的咖啡,理论上除了水和咖啡不需要添加任何其他东西。
到现在三种热饮类创建好了,我们现在分别制作这三种热饮,并看一下日至输出:
===== Begin to making HotDrinkTea =====
prepare hot water
add tea leaf
add nothing
===== Begin to making HotDrinkLatte =====
prepare hot water
add ground coffee
add milk
===== Begin to making HotDrinkAmericano =====
prepare hot water
add ground coffee
add nothing
上面的日至输出准确无误地反映了我们所定义的这三种热饮制作过程:
- 热茶:准备热水 + 茶叶
- 热拿铁:准备热水 + 咖啡 + 牛奶
- 热美式:准备热水 + 咖啡
下面看一下上面代码对应的类图。
代码对应的类图
模板方法模式代码示例类图
优点
- 复用性高:将相同的代码放在父类中,而不同的部分则由子类实现
- 扩展性高:可以通过创建不同的子类来扩展不同的算法
- 符合开闭原则:可变与不可变的部分分离,而且不同的可变部分(子类)也是相互分离的,所以符合了开闭原则
缺点
- 导致类的个数增加:对于每一个算法实现都需要一个子类,如果实现过多的话会导致类的个数增加
- 由继承关系导致的缺点:如果父类需要增加或减少它的行为,则所有的子类都需要同步修改一次
iOS SDK 和 JDK中的应用
- 在 iOS SDK 中,我们可以重写
UIView
的drawRect:
方法可以自定义绘图,是模板方法模式的一种实践。 - 在JDK中,
java.lang.Runnable
是使用JDK的经典场景:Runnable
接口可以作为抽象的命令,而实现了Runnable的线程即是具体的命令。
- ECJTUACM16 Winter vacation training #4 题解&源码
- Hadoop数据分析平台实战——090HBase shell客户端和Java Api介绍离线数据分析平台实战——090HBase shell客户端和Java Api介绍
- Hadoop数据分析平台实战——140Hive函数以及自定义函数讲解离线数据分析平台实战——140Hive函数以及自定义函数讲解
- 深入理解树状数组
- Codeforces 712C Memory and De-Evolution
- Codeforces 712B Memory and Trident
- Hadoop数据分析平台实战——110Hive介绍和Hive环境搭建离线数据分析平台实战——110Hive介绍和Hive环境搭建
- 干货|普通反爬虫机制的应对策略
- python基础-字符串与编码
- Codeforces 708A Letters Cyclic Shift
- Codeforce 712A Memory and Crow
- 每日一水之strcmp用法
- HDU 3782 xxx定律
- HDU 2566 统计硬币
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 【C语言简单说】四:常量
- 【C语言简单说】五:常用运算符
- 【C语言简单说】六:取模运算符以及变量的扩展
- 【C语言简单说】七:自定义函数(1)
- 【C语言简单说】七:自定义函数(2)
- 【C语言简单说】七:自定义函数(3)
- 【C语言简单说】八:分支结构之if(1)
- 【C语言简单说】八:分支结构之if...else...(2)
- 【C语言简单说】八:分支结构之if...else if()...else...(3)
- 【C语言简单说】九:输入
- 【C语言简单说】十:小结
- 【C语言简单说】十一:switch 补
- 【C语言简单说】十二:逻辑运算符&&
- 【C语言简单说】十三:逻辑运算符||
- 【C语言简单说】十三:变量的生命周期