定义
定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义算法的某些特定步骤。
介绍
- 模板方法模式属于行为型模式。
- 模板方法模式主要是用来定义一套流程下来的固定步骤,而具体的步骤实现则可以是不固定的。
UML类图
角色说明:
- AbstractClass(抽象类):定义了一整套算法框架。
- ConcreteClass(具体实现类):具体实现类,根据需要去实现抽象类中的方法。
实现
继续以送快递为例,快递员送快递基本就是一套固定的流程:收到快递,准备派送->联系收货人->确定结果。
1、创建抽象类。定义算法框架,这里是快递员派送快递的步骤:
// 抽象快递员类
public abstract class Postman {
// 派送流程。(这里申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序)
public final void post() {
// 准备派送
prepare();
// 联系收货人
call();
if (isSign()) {
// 签收
sign();
} else {
// 拒签
refuse();
}
}
// 准备操作,固定流程,父类实现
protected void prepare() {
System.out.println("快递已达到,准备派送");
}
// 联系收货人,联系人不一样,所以为抽象方法,子类实现
protected abstract void call();
// 是否签收,这个是钩子方法,用来控制流程的走向
protected boolean isSign() {
return true;
}
// 签收,这个是固定流程,父类实现
protected void sign() {
System.out.println("客户已签收,上报系统");
}
// 拒签,空实现,这个也是钩子方法,子类可以跟进实际来决定是否去实现这个方法
protected void refuse() {
}
}
需要注意的是上面的抽象类(Postman)包含了三种类型的方法:抽象方法、具体方法和钩子方法。
- 抽象方法:需要子类去实现。如上面的call()。
- 具体方法:抽象父类中直接实现。如上面的prepare()和sign()。
- 钩子方法:有两种,第一种,它是一个空实现的方法,子类可以视情况来决定是否要覆盖它,如上面的refuse();第二种,它的返回类型通常是boolean类型的,一般用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行,如上面的isSign()。
2、创建具体实现类。根据需要去实现抽象类中的方法,下面以派送给两个不同的人为例,其中一个签收,另一个拒收:
// 派送给A先生
public class PostA extends Postman {
// 联系收货,实现父类的抽象方法
@Override
protected void call() {
System.out.println("联系A先生并送到门口");
}
}
// 派送给B先生
public class PostB extends Postman {
// 联系收货,实现父类的抽象方法
@Override
protected void call() {
System.out.println("联系B先生并送到门口");
}
// 是否签收,覆盖父类的钩子方法,控制流程的走向
@Override
protected boolean isSign() {
return false;
}
// 拒签,覆盖父类的钩子方法
@Override
protected void refuse() {
System.out.println("拒绝签收:商品不符");
}
}
3、客户端测试
public void test() {
System.out.println("----派送A----");
Postman postA = new PostA();
postA.post();
System.out.println("----派送B----");
Postman postB = new PostB();
postB.post();
}
输出结果:
----派送A----
快递已达到,准备派送
联系A先生并送到门口
客户已签收,上报系统
----派送B----
快递已达到,准备派送
联系B先生并送到门口
拒绝签收:商品不符
应用场景
- 一次性实现算法的执行顺序和固定不变部分,可变部分则交由子类来实现。
- 多个子类中拥有相同的行为时,可以将其抽取出来放在父类中,避免重复的代码。
- 使用钩子方法来让子类决定父类的某个步骤是否执行,实现子类对父类的反向控制。
- 控制子类扩展。模板方法只在特定点调用钩子方法,这样就只允许在这些点进行扩展。
优缺点
优点
- 提高代码复用性,去除子类中的重复代码。
- 提高扩展性,不同实现细节放到不同子类中,易于增加新行为。
缺点
- 每个不同的实现都需要定义一个子类,这会导致类的个数的增加,设计更加抽象。
Android中的源码分析
Android
中View
的draw()
方法就是使用了模板方法模式:
1、View
的draw()
方法
public class View {
// 钩子方法,空实现
protected void onDraw(Canvas canvas) {
}
// 钩子方法,空实现
protected void dispatchDraw(Canvas canvas) {
}
// 绘制方法,定义绘制流程
public void draw(Canvas canvas) {
// 其他代码略
/**
* 绘制流程如下:
*
* 1. 绘制view背景
* 2. 如果有需要,就保存图层
* 3. 绘制view内容
* 4. 绘制子View
* 5. 如果有必要,绘制渐变框和恢复图层
* 6. 绘制装饰(滑动条等)
*/
// 步骤1. 绘制view背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 如果可能的话跳过第2步和第5步(常见情况)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// 步骤3. 绘制view内容
if (!dirtyOpaque) onDraw(canvas);
// 步骤4. 绘制子View
dispatchDraw(canvas);
// 覆盖一部分内容,绘制前景
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 步骤6. 绘制装饰(滑动条等)
onDrawForeground(canvas);
return;
}
}
}
2、说明
-
View
的draw()
方法中定义了一整套的绘制流程,这个流程是固定的,所有的Android
中的View
都是按照这个流程来绘制的。其中drawBackground()
这个方法在View
类中是实现了具体过程的,而onDraw()
方法和dispatchDraw()
方法在View
中都是空实现,即都是钩子方法。不同的子类通过重写这些空实现来实现自身不同的绘制效果。 - 具体的
View
,像TextView
这些单一的View
,就会重写onDraw()
方法,由于TextView
没有子View
,所以dispatchDraw()
还是空实现;而ViewGroup
类含有子View
,需要遍历子View
并绘制,因此需要重写onDraw()
和dispatchDraw()
。 - 所以,我们自定义
View
时必须且只需重写onDraw()
;自定义ViewGroup
时则需要重写onDraw()
和dispatchDraw()
。
3、其他
另外,像Activity
的生命周期,AsyncTask
等等也是用到了模板方法模式,也兴趣的可以研究一下。