满堂花醉三千客,一剑霜寒十四州
。在并发编程之AQS探秘中,我们有提到AbstractQueuedSynchroizer是基于模板方法模式设计的,那么到底什么是模板方法模式?运行的原理又是什么?下面就让我们带着这些问题一探究竟。本文包括以下部分:
- 前言
- 模板方法模式
2.1 为何要用
2.2 定义
2.3 一个例子-奶茶
2.4 添加钩子
2.5 JDK中的模板方法
2.6 模板方法VS策略模式VS工厂方法- 总结
1. 前言
面向对象世界里的三大特性。封装、继承、多态
盛名在外,无人不知无人不晓。设计模式往往也是围绕着这些特性展开的,就拿封装
来说,工厂模式封装了对象的创建、命令模式封装了方法的调用、门面模式封装了复杂接口
。接下来我们要封装算法
。
2. 模板方法模式
2.1 为什么要用
当我们用一种新东西的时候,着急去使用之前,不妨花一点时间想一想为什么要用,当我们想明白为什么要用后。才会让我们在接下来的运用中更加得心应手。
假设作者本人是个大厨😄
,大厨现在要烹饪两个菜:红烧肉和糖醋排骨。
其中红烧肉的制作
过程大致如下:
- 准备五花肉
- 倒入炒锅
- 翻炒
- 添加酱油
- 装盘
而糖醋排骨
的制作过程大致如下:
- 准备排骨
- 倒入炒锅
- 翻炒
- 添加糖
- 装盘
- 第一版代码如下
package com.moxieliunian.template;
//红烧肉
public class BraisedPork {
//食物制作
void prepareFood(){
//准备五花肉
preparePork();
//倒入炒锅
putInPan();
//翻炒
fry();
//添加酱油
addSoy();
//装盘出锅
fill();
}
void preparePork(){
System.out.println("准备红烧肉");
}
void putInPan(){
System.out.println("倒入锅中");
}
void fry(){
System.out.println("翻炒");
}
void addSoy(){
System.out.println("添加酱油");
}
void fill(){
System.out.println("装盘,制作完毕");
}
}
package com.moxieliunian.template;
//糖醋排骨
public class Ribs {
//食物制作
void prepareFood(){
//准备五花肉
prepareRibs();
//倒入炒锅
putInPan();
//翻炒
fry();
//添加糖
addSugar();
//装盘出锅
fill();
}
void prepareRibs(){
System.out.println("准备排骨");
}
void putInPan(){
System.out.println("倒入锅中");
}
void fry(){
System.out.println("翻炒");
}
void addSugar(){
System.out.println("添加糖");
}
void fill(){
System.out.println("装盘,制作完毕");
}
}
我们发现了这两个类中大量的重复代码。既然有重复的代码,这表示我们可以重新整理下设计。那么如何做呢?很自然的想法就是把公共的代码提取出来,放到一个基类中。
- 第二版代码如下:
public abstract class FoodBase {
abstract void prepareFood();
void putInPan(){
System.out.println("倒入锅中");
}
void fry(){
System.out.println("翻炒");
}
void fill(){
System.out.println("装盘,制作完毕");
}
}
//红烧肉
public class BraisedPork extends FoodBase {
@Override
void prepareFood() {
//准备五花肉
preparePork();
//倒入炒锅
putInPan();
//翻炒
fry();
//添加酱油
addSoy();
//装盘出锅
fill();
}
private void preparePork() {
System.out.println("准备红烧肉");
}
private void addSoy() {
System.out.println("添加酱油");
}
}
//糖醋排骨
public class Ribs extends FoodBase {
@Override
//食物制作
void prepareFood() {
//准备五花肉
prepareRibs();
//倒入炒锅
putInPan();
//翻炒
fry();
//添加糖
addSugar();
//装盘出锅
fill();
}
private void prepareRibs() {
System.out.println("准备排骨");
}
private void addSugar() {
System.out.println("添加糖");
}
}
此时我们发现,算法的实现流程由子类控制,那么红烧肉和糖醋排骨还有什么共通点吗?
preparePork和prepareRibs,addSoy和addSugar 只是作用的对象不一样,但是具体的作用都是相同的。
我们尝试做第三版抽象。
- 第三版代码
public abstract class FoodBase {
//规定了食物的制作流程
protected void prepareFood() {
//准备原材料
prepareMaterial();
//倒入炒锅
putInPan();
//翻炒
fry();
//添加配料
addIngredient();
//装盘出锅
fill();
}
//准备原材料
protected abstract void prepareMaterial();
//添加配料
protected abstract void addIngredient();
private void putInPan() {
System.out.println("倒入锅中");
}
private void fry() {
System.out.println("翻炒");
}
private void fill() {
System.out.println("装盘,制作完毕");
}
}
//红烧肉
public class BraisedPork extends FoodBase {
@Override
protected void prepareMaterial() {
System.out.println("准备红烧肉");
}
@Override
protected void addIngredient() {
System.out.println("添加酱油");
}
}
//糖醋排骨
public class Ribs extends FoodBase {
@Override
protected void prepareMaterial() {
System.out.println("准备排骨");
}
@Override
protected void addIngredient() {
System.out.println("添加糖");
}
}
我们将preparePork和prepareRibs,addSoy和addSugar 抽象为prepareMaterial和addIngredient
,此时类图如下:
问:第三版代码(模板方法)相比前两版而言,有什么好处?
1. 算法的骨架由基类(FoodBase)规定且保护,且只存在基类中,后期修改只需要修改一处即可,便于维护。2. 子类只需要覆盖其需要覆盖的方法,可以达到最大化的代码复用。3. 同时可以时间算法的实现和算法本身想分离
2.2 定义
明白了上面的例子,我们就了解了模板方法的原理。模板方法在一个方法中定义了算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
就拿我们上面的例子来说,模板方法FoodBase.prepareFood()规定了食物的制作流程为:准备原料、倒入炒锅、翻炒、添加配料、出锅。子类只需要实现准备原料和添加配料的方法即可。所有的子类食物制作方法都是按照这个流程进行。
2.3 一个例子-奶茶
现在有个奶茶店,生产两种奶茶:珍珠奶茶和红豆奶茶
,其中珍珠奶茶制作流程如下:
- 准备奶和茶
- 倒入杯子
- 添加珍珠
- 加冰
- 密封
而红豆奶茶制作如下
- 准备奶和茶
- 倒入杯子
- 添加红豆
- 加冰
- 密封
这两个过程有着相同的算法骨架,我们很容易想到用模板方法模式来实现,如下:
//奶茶算法基类
public abstract class TeaBase {
protected void prdouceTea(){
//准备奶和茶
prepareMilkAndTea();
//倒入杯子
putInCup();
//添加配料
addIngredient();
//加冰
addIce();
//密封打包
box();
}
private void prepareMilkAndTea(){
System.out.println("准备奶和茶");
}
private void putInCup(){
System.out.println("倒入杯子");
}
protected abstract void addIngredient();
private void addIce(){
System.out.println("加冰");
}
private void box(){
System.out.println("密封");
}
}
//珍珠奶茶
public class BubbleTea extends TeaBase{
@Override
protected void addIngredient() {
System.out.println("添加珍珠");
}
}
//红豆奶茶
public class RedBeanTea extends TeaBase{
@Override
protected void addIngredient() {
System.out.println("添加红豆");
}
}
使用我们的算法
BubbleTea bubbleTea=new BubbleTea();
RedBeanTea redBeanTea=new RedBeanTea();
System.out.println("珍珠奶茶制作开始");
bubbleTea.prdouceTea();
System.out.println("珍珠奶茶制作结束");
System.out.println("红豆奶茶制作开始");
redBeanTea.prdouceTea();
System.out.println("红豆奶茶制作结束");
珍珠奶茶制作开始
准备奶和茶
倒入杯子
添加珍珠
加冰
密封
珍珠奶茶制作结束
红豆奶茶制作开始
准备奶和茶
倒入杯子
添加红豆
加冰
密封
红豆奶茶制作结束
可以看到算法按照我们规定的骨架,正确执行了
2.4 添加钩子
上面的例子中,我们使用模板方法模式来实现了奶茶的制作流程。但是有一个问题,无论是珍珠奶茶还是红豆奶茶,都默认加冰。如果要制作一杯不加冰的奶茶该如何操作呢?我们可以添加一个钩子,Hook
。先看怎么用。
//奶茶算法基类
public abstract class TeaBase {
protected void prdouceTea(){
//准备奶和茶
prepareMilkAndTea();
//倒入杯子
putInCup();
//添加配料
addIngredient();
//默认加冰
if (isNeedIce()){
//加冰
addIce();
}
//密封打包
box();
}
private void prepareMilkAndTea(){
System.out.println("准备奶和茶");
}
private void putInCup(){
System.out.println("倒入杯子");
}
protected abstract void addIngredient();
//是否加冰,默认是
protected boolean isNeedIce(){
return true;
}
private void addIce(){
System.out.println("加冰");
}
private void box(){
System.out.println("密封");
}
}
//由客户自己决定是否加冰的珍珠奶茶
public class SmartBubbleTea extends TeaBase{
private boolean isNeedIce;
SmartBubbleTea(boolean isNeedIce){
super();
this.isNeedIce=isNeedIce;
}
@Override
protected void addIngredient() {
System.out.println("添加珍珠");
}
@Override
public boolean isNeedIce() {
return isNeedIce;
}
}
使用
SmartBubbleTea smartBubbleTea = new SmartBubbleTea(false);
System.out.println("不加冰珍珠奶茶制作开始");
smartBubbleTea.prdouceTea();
System.out.println("不加冰珍珠奶茶制作结束");
不加冰的珍珠奶茶制作开始
准备奶和茶
倒入杯子
添加珍珠
密封
不加冰的珍珠奶茶制作结束
可以看到,我们利用hook成功实现了,奶茶加不加冰的自由控制。
hook的作用有以下几点:
-
钩子可以让子类实现算法的可选部分,或者在钩子对于子类的实现不重要的时候,子类可以对这个钩子置之不理
。 -
钩子可以让子类对模板中即将发生的东西做出反应
。
2.5 JDK中的模板方法
上面说了那么多,都是我们自己在写demo。那么JDK中有没有用到模板方法的地方呢?当然是有的,比如:AbstractQueuedSynchronizer.acquire(int arg)、AbstractList.addAll(int index, Collection<? extends E> c)、Arrays.sort(Object[] a)
等,以Arrays.sort为例,找出其中的模板方法
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
private static void binarySort(Object[] a, int lo, int hi, int start) {
assert lo <= start && start <= hi;
if (start == lo)
start++;
for ( ; start < hi; start++) {
Comparable pivot = (Comparable) a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
assert left <= right;
/*
* Invariants:
* pivot >= all in [lo, left).
* pivot < all in [right, start).
*/
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
assert left == right;
可以看到,sort方法调用的时候,依赖于传入对象的compareTo方法,也就是说sort方法规定了排序的执行骨架,而由具体的类去决定排序的算法细节(重写compareTo)
。这里之没有用到继承,一方面是因为数组不能被继承,另一方面则是因为排序方法希望能作用于所有的数组
。
我们可以看出设计模式往往并不是一尘不变的,使用中往往伴随着不同场景的适配。
2.6 模板方法VS策略模式VS工厂方法
模板方法模式和策略模式都是跟算法有关,那么这两个设计模式之间有什么异同呢?模板方法模式和工厂方法模式又有什么异同呢?
名称 | 定义 | 区别与联系 |
---|---|---|
策略模式 | 定义了算法族,分别封装起来,让它们之间可以互换。 | 与模板方法一样都是作用于算法 ,不同的是策略模式侧重于算法的替换,使用组合 来实现。 |
模板方法模式 | 在一个方法中定义了算法的骨架,将某些实现推迟到子类中,使子类可以改变实现细节,而不影响整体架构 | 与策略模式一样,都是作用于算法 。不同的是模板方法侧重于子类改变算法的某个细节,而不会改变模板的整个算法骨架,使用继承 实现。 |
工厂方法模式 | 定义了一个创建对象的接口,但由子类决定要实例化的是哪一个,将类的实例化推迟到子类中 | 工厂方法是模板方的一个特殊版本 |
3. 总结
本篇文章中,我们探讨了模板方法模式的相关内容。并和与之相似的设计模式:策略模式、工厂方法模式进行了简单对比。
由于技术水平所限,文章难免有不足之处,欢迎大家指出。我们下一篇文章见.....
参考文章
head first 设计模式