设计模式 ~ 行为型模式 ~ 模板方法模式 ~ Template Pattern。
what。
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的
环境相关。
eg. 去银行办理业务一般要经过以下 4 个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现(final),但是办理具体业务却因人而异,ta 可能是存款、取款或者转账等,可以延迟到子类中实现。
定义:
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
结构。
模板方法(Template Method)模式包含以下主要角色。
-
抽象类(Abstract Class)。
负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。- 模板方法。
定义了算法的骨架,按某种顺序调用其包含的基本方法。 - 基本方法。
是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种。- 抽象方法(Abstract Method)。
一个抽象方法由抽象类声明、由其具体子类实现。 - 具体方法(Concrete Method)。
一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。 - 钩子方法(Hook Method)。
在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为 isXxx();,返回值类型为 boolean 类型。
- 抽象方法(Abstract Method)。
- 模板方法。
-
具体子类(Concrete Class)。
实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
【eg.】炒菜。
package com.geek.templete.method.pattern;
/**
* 抽象类 ~ 定义模板方法和基本方法。
*
* @author geek
*/
public abstract class AbstractClass {
/**
* 模板方法。
* 定义算法结构。final 不让子类更改。
*/
public final void cookProcess() {
// 第一步:倒油。
this.pourOil();
// 第二步:热油。
this.heatOil();
// 第三步:倒蔬菜。
this.pourVegetable();
// 第四步:倒调味料。
this.pourSauce();
// 第五步:翻炒。
this.fry();
}
/**
* 第一步:倒油。是一样的,所以直接实现。
*/
private void pourOil() {
System.out.println("倒油。");
}
/**
* 第二步:热油。是一样的,所以直接实现。
*/
private void heatOil() {
System.out.println("热油。");
}
/**
* 第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)。
*/
public abstract void pourVegetable();
/**
* 第四步:倒调味料是不一样。
*/
public abstract void pourSauce();
/**
* 第五步:翻炒是一样的,所以直接实现。
*/
private void fry() {
System.out.println("炒啊炒啊炒到熟啊。");
}
}
package com.geek.templete.method.pattern;
/**
* 炒包菜。
*
* @author geek
*/
public class ConcreteClassBaocai extends AbstractClass {
/**
* 第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)。
*/
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜。");
}
/**
* 第四步:倒调味料是不一样。
*/
@Override
public void pourSauce() {
System.out.println("下锅的酱料是辣椒。");
}
}
package com.geek.templete.method.pattern;
/**
* 炒菜心。
*
* @author geek
*/
public class ConcreteClassCaixin extends AbstractClass {
/**
* 第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)。
*/
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心。");
}
/**
* 第四步:倒调味料是不一样。
*/
@Override
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉。");
}
}
package com.geek.templete.method.pattern;
/**
* @author geek
*/
public class Client {
public static void main(String[] args) {
// 炒包菜。
// 创建对象。
ConcreteClassBaocai baocai = new ConcreteClassBaocai();
// 调用炒菜的功能。
baocai.cookProcess();
}
}
/*
Connected to the target VM, address: '127.0.0.1:61766', transport: 'socket'
倒油。
热油。
下锅的蔬菜是包菜。
下锅的酱料是辣椒。
炒啊炒啊炒到熟啊。
Disconnected from the target VM, address: '127.0.0.1:61766', transport: 'socket'
Process finished with exit code 0
*/
优点。
-
提高代码复用性。
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。 -
实现了反向控制。
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点。
对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,ta 提高了代码阅读的难度。
适用场景。
-
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
-
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。(需要定义钩子函数)。
JDK 源码解析 ~ InputStream 类。
package java.io;
/**
* This abstract class is the superclass of all classes representing
* an input stream of bytes.
*
* <p> Applications that need to define a subclass of <code>InputStream</code>
* must always provide a method that returns the next byte of input.
*
* @author Arthur van Hoff
* @see java.io.BufferedInputStream
* @see java.io.ByteArrayInputStream
* @see java.io.DataInputStream
* @see java.io.FilterInputStream
* @see java.io.InputStream#read()
* @see java.io.OutputStream
* @see java.io.PushbackInputStream
* @since JDK1.0
*/
public abstract class InputStream implements Closeable {
// MAX_SKIP_BUFFER_SIZE is used to determine the maximum buffer size to
// use when skipping.
private static final int MAX_SKIP_BUFFER_SIZE = 2048;
// 抽象方法。要求子类必须重写。
public abstract int read() throws IOException;
// 一个参数的方法,重载,调用 3 个参数的方法。
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
// 调用的无参 read(); 抽象方法 ~ 子类重写。(模板方法思想:反向控制)。
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
// 每次读取一个字节放入字节数组。循环 len 次。
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
public long skip(long n) throws IOException {
long remaining = n;
int nr;
if (n <= 0) {
return 0;
}
int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
byte[] skipBuffer = new byte[size];
while (remaining > 0) {
nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
if (nr < 0) {
break;
}
remaining -= nr;
}
return n - remaining;
}
public int available() throws IOException {
return 0;
}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
}
从上面代码可以看到,无参的 read(); 方法是抽象方法,要求子类必须实现。而 read(byte b[]); 方法调用了 read(byte b[], int off, int len); 方法,所以在此处重点看的方法是带三个参数的方法。
在该方法中,可以看到调用了无参的抽象的 read() 方法。
在 InputStream 父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取 len 个字节数据。具体如何读取一个字节数据呢?由子类实现。