定义
Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
在一个操作中定义算法的框架,将某些步骤延迟到子类中。让子类在不改变算法结构的前提下就可以重新定义该算法的某些特定步骤。
小故事
今天,接到了派出所的传换电话,说我涉嫌违反网络安全法,要我过去配合调查。
吃了午饭,买了杯咖啡,我就去了;进门后,说明来意,便进了一小房间。
咖啡喝完,来了一人,马尾辫,殷桃嘴,坐到了我对面,就开始了对我的调查:
她:先生,请你回忆一下当时是如何爬取A公司数据的?
我:建一个类,写个方法,先获取对方的HTML数据,后转换数据,再清洗数据,最后保存数据。
//获取数据
String html=httpClient.get("A公司数据地址");
//转换数据
List<Data> dataList = HtmlUtil.convert(html);
//清洗数据
List<Data> clearedDataList = DataUtil.clear(dataList);
//保存数据
dataBase.save(clearedDataList);
她:那又是如何获取B公司数据的?
我:复制上面介绍过的类,修改了一下获取数据这个步骤,因为B公司的数据是JSON格式的。
她:还有C公司的数据,又是如何爬取的?
我:过程都差不多,也是复制然后修改一下获取数据这个步骤,因为C公司的数据是XML格式的。
她:嗯,笔录做完了,你先回去接娃放学吧。
我:好的,老婆!
前几天,我和她说我在为公司写爬虫,她一听吓坏了,便把我举报了。
我再怎么解释我们公司是遵守机器人协议的,她就是不听,非得大义灭亲。
问题
言归正传,在故事中显而易见的是,爬虫的实现流程有四个步骤构成:获取数据、转换数据、清洗数据、保存数据,它们彼此之间存在着一定的次序和依赖关系,其中有些步骤会因数据格式的变化而变化,而有些步骤则是固定不变的。
同样在故事中,也能发现一些问题:除了转换数据这一步外其余步骤的代码都是相同的,而且客户端不能多态地使用这三个实现类。如果要修改相同的步骤,那就要修改多个类的代码。
总之,上面的程序没有对步骤进行分离、抽象、编排导致了代码重复、扩展复杂。这时,我们应该考虑使用模版方法模式。
方案
在模版方法模式中,首先会将流程拆分成独立的步骤并定义成抽象类中的步骤方法,然后会定义一个模版方法将这些步骤方法按一定的次序和条件组织起来。模版方法对客户端是可见的,步骤方法对客户端不可见。如果方法是通用的会为其提供默认的实现,如果是特定的会被声明为抽象方法并由子类实现。
如果要处理Excel格式的数据,那只需继承抽象类并实现特定的方法(抽象方法)。但如果需要修改通用方法,那也只需覆盖父类的方法。可以看出该模式不仅可以复用步骤,而且无需关心具体的处理次序以及步骤之间的依赖关系——这正是模版方法模式的独特之处。
应用
接下来,我们使用模版方法模式重构一下"爬虫"。
首先,在抽象类中定义步骤方法和模版方法,以及为通用的步骤提供默认的实现。
/**抽象模版类*/
public abstract class Robot {
public final void process(String url){
String strData = getData(url);
List<Data> data = convert(strData);
if(data.size()>0){
clearData(data);
if(data.size()>0){
saveData(data);
}
}
}
/**获取数据*/
protected String getData(String url){
return HttpClient.httpGet(url);
}
/**转换数据*/
abstract List<Data> convert(String originalData);
/**清洗数据*/
protected void clearData(List<Data> dataList){
System.out.println("清洗数据");
}
/**保存数据*/
protected void saveData(List<Data> dataList){
System.out.println("保存数据");
}
}
然后,创建各种数据格式的爬虫HTMLRobot、JSONRobot、XMLRobot。
/**Html格式的爬虫*/
public class HTMLRobot extends Robot{
@Override
List<Data> convert(String originalData) {
System.out.println("将原始数据转换成标准的数据格式");
return new LinkedList<>();
}
}
/**Xml格式的爬虫*/
public class XMLRobot extends Robot{
@Override
List<Data> convert(String originalData) {
return new LinkedList<>();
}
}
/**Json格式的爬虫*/
public class JSONRobot extends Robot{
@Override
List<Data> convert(String originalData) {
return new LinkedList<>();
}
@Override
protected void saveData(List<Data> dataList) {
System.out.println("覆盖默认实现,将数据保存在ES中");
Elasticsearch.save(dataList);
}
}
最后,我们在看看客户端如何使用模版方法模式。
public class Client {
public static void main(String[] args) {
//爬取A公司的HTML数据
HTMLRobot htmlRobot = new HTMLRobot();
htmlRobot.process("A公司地址");
//爬取B公司的HTML数据
JSONRobot jsonRobot = new JSONRobot();
jsonRobot.process("B公司地址");
//爬取C公司的HTML数据
XMLRobot xmlRobot = new XMLRobot();
xmlRobot.process("C公司地址");
}
}
结构

模版角色(Abstract Class) :负责定义模版方法和步骤方法。模版方法负责组织操作流程中的各个步骤。步骤方法可分为三种:抽象方法,通用方法,钩子方法,抽象方法是特定的方法由子类实现,通用方法是子类可复用、可覆盖的方法,钩子方法是一个空实现方法。
步骤角色(Concrete Class) :负责实现抽象方法,根据具体情况重写抽象类中的通用方法或实现钩子方法。
/**模版类*/
public abstract class AbstractClass {
public void templateMethod(){
if(step1()){
step2();
}
hookStep3();
step4();
}
abstract public boolean step1();
public void step2(){
System.out.println("默认实现");
}
public void hookStep3(){}
public void step4(){
System.out.println("默认实现");
}
}
/**步骤实现类*/
public class ConcreteClass extends AbstractClass{
@Override
public boolean step1() {
System.out.println("实现抽象方法");
return false;
}
@Override
public void hookStep3() {
System.out.println("实现钩子方法");
}
@Override
public void step4() {
System.out.println("覆盖父类方法");
}
}
总结
当一个操作的实现流程由许多步骤构成,且大多数步骤可以复用,那么应该将这些步骤抽象出来并组织起来,而且还要为通用的步骤提供默认的实现。
这样,在扩展抽象时,只需实现特定的步骤而无需关心整体的流程,大大降低了扩展的复杂度。
网友评论