定义
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。
使用场景
对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
结构
模式所涉及的角色有:
抽象类(AbstractClass):实现了模板方法,定义了算法的骨架。
具体类(ConcreteClass):实现抽象类中的抽象方法,已完成完整的算法。
模板方法中的方法可以分为两大类:模板方法和基本方法。
模板方法
一个模板方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。
一个抽象类可以有任意多个模板方法,而不限于一个。每一个模板方法都可以调用任意多个具体方法。
基本方法
基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在Java语言里抽象方法以abstract关键字标示。
具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或置换。
钩子方法:一个钩子方法由抽象类声明并实现,而子类会加以扩展。通常抽象类给出的实现是一个空实现,作为方法的默认实现。
实现
分析完理论,我们用一个饮料店出售饮料的小例子来分析一下模板方法模式。夏天我们去饮料店购买饮料,告诉服务员要一大杯奶茶,还要加冰。
那么饮料店是如何装配我们的饮料的呢?首先准备一个大杯子,然后入我们需要的饮料,如果加冰会放入一些冰块,最后盖好盖子打包给我们。
仔细思考一下我们的使用场景,有没有发现很符合呢?接下来我们用代码实现一下。
抽象方法,装配饮料的模板
/**
* 饮料模板
* Created by lei on 2016/10/19.
*/
public abstract class TemplateDrink {
/**
* 获取饮料的方法,用final修饰,防止被子类修改算法结构。模板方法
*/
public final void getDrink() {
getGlass();
getContent();
if (hook()) {
Log.e(TAG, "加冰--->");
} else {
Log.e(TAG, "不加冰--->");
}
pack();
}
/**
* 获取杯子,杯子分大中小杯
*/
public abstract void getGlass();
/**
* 获取具体种类饮料
*/
public abstract void getContent();
/**
* 夏天我们的饮料是默认加冰的。钩子方法
*/
public boolean hook() {
return true;
}
/**
* 打包
*/
public void pack() {
Log.e(TAG, "包装");
}
}
这里需要注意的是getDrink()方法是用final修饰的,这样就保证了逻辑流程不会被子类修改,子类只能修改某一个步骤的具体实现。
具体某种饮料的实现
/**
* 具体的饮料,比如奶茶
* Created by lei on 2016/10/19.
*/
public class ConcreteDrink extends TemplateDrink {
@Override
public void getGlass() {
Log.e(TAG, "中杯--->");
}
@Override
public void getContent() {
Log.e(TAG, "奶茶--->");
}
@Override
public boolean hook() {
return super.hook();
}
}
这个模式相对来说比较简单,但是应用却非常广泛,毕竟越简单越容易应用嘛。比如在重构的时候,把相同的代码抽取到父类中,然后配合钩子函数约束其行为。
当然在Android源码中的应用肯定也不少,比如AsyncTask的实现,调用execute后会依次执行onPreExecute、doInBackground、onPostExecute,还可以通过onProgressUpdate来更新进度,它的执行过程其实是一个框架,类似的还有Activity的生命周期回调方法的执行。
接下来我们看一个比较明显的,即View中的Draw()方法。
我们先看下方法注释
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
即在我们调用layout()方法之后,可以调用draw()方法来手动渲染view(包括它的子view)到传入的canvas上。如果我们继承View类的话需要重写ondraw()方法而不是draw()。如果你必须重写,需要调用超类的版本。
draw()方法比较长,我们挑主要的看一下。
方法开头有一段注释是这样的:
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas’ layers to prepare for fading
* 3. Draw view’s content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
遍历执行几个绘制步骤,并且这些步骤需要按适当顺序执行。
我们重点看下3和4
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
具体方法如下
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}
我们看一下View类族的UML图
子类只能通过扩展onDraw(Canvas canvas)和dispatchDraw(Canvas canvas)两个函数,使子类的显示效果和别的具体子类。
接下来对号入座,draw()对应我们的模板方法,ondraw()和dispatchDraw()就是钩子方法。
搞定,收工。
测试代码已上传到github。