@xiu-tanwy
2016-11-11T09:34:45.000000Z
字数 3025
阅读 339
java-design-pattern
模板方法(Template method),顾名思义,就是做一些任务的通用流程。如网上有许多自我介绍模板、推荐信模板,即开头和结尾可能都是差不多的内容,而中间需要客户去修改一下即可使用。设计模式源自生活,模板方法就在类似的场景下诞生了。模板方法是指写一个操作中的算法框架,而将一些步骤延迟到子类中去实现,这样就使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法通常会设计一个抽象类,内部定义一些需要子类去实现的抽象方法,因为这些方法可能因不同的子类会有不同的实现,因此定义为抽象方法。另外,抽象类中应该有一个或多个模板方法,即固定好的框架,而且这个方法的修饰符通常定义为final,这样做是为了防止子类去覆写这个模板,因为我们认为模板是固定的,不容修改。而在这个固定的模板方法内部,会调用那些抽象方法,来一步一步实现整个算法的流程。通常,子类要去继承这个抽象父类,并根据自己的业务逻辑实现里面的抽象方法。
模板方法清晰地划定了某一类业务的变与不变,为一类业务做好了流程框架,为子类提供了公共的代码,并且子类的行为完全由父类来控制,实现了代码的可维护性和可扩展性。父类不容修改,子类可以去扩展,很好地符合了设计模式的开闭原则——对修改封闭,对扩展开放。
注意模板方法设计模式与抽象类设计的区别,抽象类这种设计模式是父类定义一些抽象方法,让子类去实现,因此子类通常有更多的自由空间;而模板方法中是父类定义好了算法框架,子类去实现父类其中的抽象方法,因此子类的作用可以影响父类。
假设现在要制作一些饮料产品,比方说要泡茶和咖啡。泡茶和泡咖啡的流程大体上可以分为四步,第一将水煮沸,第二烘焙原料,第三倒入杯中,第四加入调料。通常第一步和第三步动作是一样的,所以我们可以在父类中将方法直接写好,而第二步和第四步则随着泡茶还是泡咖啡有所不同,因此我们设计为抽象方法,让子类去实现。而这四步整体上又是泡饮料的固定流程,所以我们将这四步封装在一个方法中,并且设置这个方法的修饰符为final,以防子类去修改它。
下面先写出模板方法的代码:
package com.template;
/**
* 模板模式
* 抽象基类,为所有子类提供算法框架
* 业务:提神饮料
* @author zzw
*
*/
public abstract class RefreshBeverage {
/*
* 制备饮料的模板方法,指定算法框架
*/
//阻止子类对模板方法进行复写
public final void prepareBeverageTemplate() {
//步骤1:将水煮沸
boilWater();
//步骤2:炮制饮料
brew();
//步骤3:倒入杯中
pourInCup();
//步骤4:加入调料(引入钩子函数,从用户角度出发,可选择性)
if(isCustomerWantCondiments()) {
addCondiments();
}
}
/*
* Hook,钩子函数
* 提供一个默认或空的实现
* 具体子类可以自行决定是否挂钩以及如何挂钩
* 询问用户是否加入调料
*/
protected boolean isCustomerWantCondiments() {
// 默认设置
return true;
}
//抽象方法,由子类实现
protected abstract void addCondiments();
private void pourInCup() {
// 倒入杯中
System.out.println("倒入杯中");
}
//抽象方法,由子类实现
protected abstract void brew();
private void boilWater() {
// 将水煮沸
System.out.println("将水煮沸");
}
}
由于泡茶喝泡咖啡的具体实现有所不同,并且还可以使用钩子函数来判断用户是否需要加调料,这样使得流程更具人性化。
泡茶:
package com.template;
/**
* 茶叶制备的具体实现
* 子类
* @author zzw
*
*/
public class TeaBeverage extends RefreshBeverage {
@Override
protected void addCondiments() {
// TODO Auto-generated method stub
System.out.println("加入茶叶调料");
}
@Override
protected void brew() {
// TODO Auto-generated method stub
System.out.println("烘焙茶叶");
}
}
泡咖啡
package com.template;
public class CoffeeBeverage extends RefreshBeverage {
@Override
protected void addCondiments() {
// TODO Auto-generated method stub
System.out.println("加入咖啡调料");
}
@Override
protected void brew() {
// TODO Auto-generated method stub
System.out.println("烘焙咖啡");
}
}
由于中国茶一般不加调料,所以这里如果引入一个中国茶的话,在继承茶的基础上,将是否加调料设为不加即可。
package com.template;
/**
* 制备中式茶
* 不需要加调料
* 因此选择挂钩函数
* 其他的则继承茶的制作
* @author Administrator
*
*/
public class ChineseTeaBeverage extends TeaBeverage {
@Override
protected boolean isCustomerWantCondiments() {
// TODO Auto-generated method stub
return false;
}
}
钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。
对每种类型的钩子由系统来维护一个钩子链,最近安装的钩子放在链的开始,而最先安装的钩子放在最后,也就是后加入的先获得控制权。如本博文中提到的是否加入调料即为一种钩子函数,它对是否加调料具有决定权。
如果指定确定的线程,即为线程专用钩子;如果指定为空,即为全局钩子。其中,全局钩子函数必须包含在DLL(动态链接库)中,而线程专用钩子还可以包含在可执行文件中。得到控制权的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外一个SDK中的API函数CallNextHookEx来传递它。钩子函数也可以通过直接返回TRUE来丢弃该消息,并阻止该消息的传递。
模板方法可以用于一次性实现一个算法的不变的部分,并将可变的部分留给子类去实现;子类的公共代码部分应该被提炼到父类中去写好,防止代码重复编写;控制子类的扩展,模板方法只允许在特定点调用钩子函数,这样就只允许在这些点进行扩展。
其实这个例子还不够生动。模板方法模式还有很牛逼的使用场景。