设计之禅——模板方法模式
一、引言
模板方法模式在我们平时开发中是非常常见,也是非常容易理解的,在平时不经意间就会使用到,因此理解其设计思想是非常有必要的。
二、定义
在《Head First设计模式》一书中是如下定义模板方法模式的:
模板方法模式是在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类当中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
通俗一点说也就是,我们需要定义一个固定的算法步骤,而每个步骤则可以让客户通过继承来实现个性化自定义,这样也就遵循了对扩展开放,对修改关闭原则,极大程度的实现代码复用以及保证代码的扩展性。
同时模板方法模式还需要遵循一个原则:好莱坞原则。那什么是好莱坞原则呢?
别调用我们,我们会调用你。
这是高层组件(父类)对待低层组件的方式,简单的说就高层组件可以调用低层组件,低层组件不允许直接调用高层组件。为什么要这样呢?想象一下,高层组件依赖低层组件,低层组件依赖于高层组件,而高层组件又依赖于侧边组件,侧边组件又依赖于低层组件……形成“依赖腐败”,也就是环状依赖,在这种情况下,想要轻易的理解系统的设计就非常的难了,所以良好的设计应该遵循好莱坞原则。
三、代码实现
模板方法模式对应生活中的实例也非常的多,这里我以做饭炒菜来说明。炒菜都需要经历洗菜、切菜、炒菜、出锅等几个过程,因此这是一个固定的过程,我将其抽象为炒菜前准备、炒菜中以及摆盘三个过程作为算法的骨架,放到doCooking方法中,并将该方法设置为final,保证其行为不会被改变;preCooking()和putPlate()假定都是一样的,而具体要做什么菜则是由子类来实现,代码如下:
public abstract class Vegetables {
public final void doCooking() {
preCooking();
cooking();
putPlate();
}
private void preCooking() {
System.out.println("Wash vegetables and light a fire!");
}
protected abstract void cooking();
private void putPlate() {
System.out.println("Put plate!");
}
}
public class Potato extends Vegetables {
@Override
protected void cooking() {
System.out.println("炸土豆片!");
}
}
代码非常简单,客户点什么菜,则实例化相应的子类,再调用父类的doCooking方法就能做出相应的菜。假如没有模板方法,那么每一道菜都要去实现全部的流程,大量重复的工作将是一场灾难,代码看起来也会非常的臃肿,而通过模板方法我们就将变化的部分解耦出来,大大的减少代码量,实现代码的复用。但是,我这里只有一个抽象的过程,如果父类中定义了多个抽象的过程呢?那其下所有子类也需要将所有的方法实现一遍,因此在抽象步骤的划分时一定要掌握好度。
上面的实现看起来相当不错了,但是它只是一个最基本的实现,并不能满足实际复杂的业务。当某个步骤并不是必须的时候该如何处理呢?比如,当客户点了一道“凉拌黄瓜”,但有的客户要求去皮,有的不用。思考一下在上面的例子中要如何做。我们只需要在父类中加入一个“钩子”方法来作为条件控制就行了,如下:
public abstract class Vegetables {
public final void doCooking() {
preCooking();
if (isPeel()) {
peel();
}
cooking();
putPlate();
}
private void preCooking() {
System.out.println("Wash vegetables and light a fire!");
}
private void peel() {
System.out.println("Peel!");
}
protected abstract void cooking();
private void putPlate() {
System.out.println("Put plate!");
}
// 钩子方法
protected boolean isPeel() {
return true;
}
}
父类中增加一个peel步骤,该步骤由钩子方法isPeel()来控制是否需要执行,父类一般默认控制需要或不需要,而真正的条件逻辑应由子类覆盖该方法来实现:
public class Cucumber extends Vegetables {
@Override
protected void cooking() {
System.out.println("凉拌黄瓜!");
}
@Override
protected boolean isPeel() {
Scanner sc = new Scanner(System.in);
String str = sc.next();
if ("n".equals(str)) {
return false;
}
return true;
}
}
每次都询问客户是否需要“去皮”,当客户输入“n”时,不执行peel()方法,否则都会执行,其它菜品可以覆盖此方法也可以不覆盖,不覆盖默认也会去皮。也就使得步骤的执行与否交由子类来控制,更加的灵活。
四、总结
模板方法模式实现很简单,理解起来也不难,实际应用也非常广泛,但它也不是完美无缺的,像上面所说的抽象步骤一旦划分过多,实现起来也是相当的繁复,而且,每增加一个实现都需要添加一个类,也就使得系统越来越庞大。因此在实际设计过程中也需要好好考虑。
同时模板方法在实际中常常会有许多的变种,像java api中的Arrays.sort()方法以及上次讲的工厂方法其实都是模板方法模式实现的,我们需要真正的理解每种模式的设计思想,多看优秀的代码设计,才能应对万般变化。最后贴上《Head First设计模式》中对于模板方法模式总结的要点,共同进步!
- 模板方法定义了算法的步骤,把这些步骤的实现延迟到子类;
- 为了防止子类改变模板方法中的算法,可以将模板方法定义为final;
- 钩子是一种方法,它在抽象类中不做事,或只做默认的事,子类可以选择是否覆盖;
- 好莱坞原则告诉我们,将决策权放在高层组件中,以便决定如何以及何时调用低层组件;
- 策略模式和模板方法都封装算法,一个用组合,一个用继承;
- 工厂方法是特殊的模板方法。