源起
很多设计模式比较简单,我们在设计的时候或许都用过,只是不知道它们的名字而已。模板方法模式就是其中一种,类似的还有享元模式。或许听这名字你会觉得不知所云,但等真正理解其原理的时候你肯定会排着大腿说 :OMG,原来这就是模板方法模式。那读者不禁要问了:为什么这么多设计模式你不写,只写模板方法模式,是不是因为它简单呀?对呀,柿子要挑软的捏。开个玩笑,最主要原因最近在工作中用到了这种设计模式,而且即使就这么简单的设计模式还是几经周折才用上,还是经验不够丰富。话不多说,放码过来。本文主要分为两个部分:
- 模板方法模式相关内容的介绍。
- 扩展(一些碎言碎语,可跳过)。
什么是模板方法模式
脑筋急转弯
小明将大象装进冰箱需要几个步骤,知道这个脑筋急转弯的人肯定可以快速的答出有如下三个步骤:
- 小明把冰箱门打开
- 小明把大象放进去
- 小明把冰箱门关上
用 java 实现如下
class XiaoMingStoreElephantToFridge {
public void xiaoMingOpenFridgeDoor() {
System.out.println("小明打开冰箱门");
}
public void xiaoMingPutIntoFridge() {
System.out.println("小明将大象装进冰箱");
}
public void xiaoMingCloseFridgeDoor() {
System.out.println("小明关上冰箱门");
}
//更多时候我们用一个方法将以上步骤封装起来,在主方法中(client中)一次调用该方法即可。
public void xiaoMingStoreElementToFridge(){
xiaoMingOpenFridgeDoor();
xiaoMingPutIntoFridge();
xiaoMingCloseFridgeDoor();
}
}
复制代码
那小红把蚂蚁装进冰箱需要几个步骤呢,也是三步(不用把大象拿出来):
- 小红把冰箱门打开
- 小红把蚂蚁放进去
- 小红把冰箱门关上
用 java 实现如下
class XiaoHongStoreAntToFridge {
public void xiaoHongOpenFridgeDoor() {
System.out.println("小红打开冰箱门");
}
public void xiaoHongPutIntoFridge() {
System.out.println("小红将蚂蚁装进冰箱");
}
public void xiaoHongCloseFridgeDoor() {
System.out.println("小红关上冰箱门");
}
//更多时候我们用一个方法将以上步骤封装起来,在主方法中(client中)一次调用该方法即可。
public void xiaoHongStoreElementToFridge(){
xiaoHongOpenFridgeDoor();
xiaoHongPutInfoFridge();
xiaoHongCloseFridgeDoor();
}
}
复制代码
那把 xxx 放入冰箱需要几步,这下你可以毫不犹豫的说出需要三步,Balabala。稍微有点设计思想的都知道我们可以将小明/小红将大象/蚂蚁装进冰箱做进一步抽象,如下:
- 将冰箱门打开。
- 将xxx放进冰箱。
- 将冰箱门关闭。
同理,java 类如下:
class AbstractStoreSomethingToFridge {
protected abstract void penFridgeDoor();
protected abstract void putIntoFridge();
protected abstract void closeFridgeDoor();
}
//更多时候我们用一个方法将以上步骤封装起来,在主方法中(client中)一次调用该方法即可。
public final void store() {
penFridgeDoor();
putIntoFridge();
closeFridgeDoor();
}
}
复制代码
该抽象类只关心将物体放入冰箱的有哪些步骤,至于是谁/将什么放进冰箱该抽象类并不关心,这是子类该做的事。为了不占用过多无用代码,子类这里不再赘述。
模板方法模式的定义
以上 AbstractStoreSomethingToFridge 就是一个模板类,其中 store() 方法就是模板方法。模板方法定义了一个方法的步骤,并允许子类为一个或者多个步骤提供实现。模板方法一般用 final 修饰,表示不允许在子类中重写该方法。如果模板方法可以被重写,那"模板"将毫无意义,或者说如果要重写模板方法那说明你要重定义一个模板类了。模板方法模式的 UML 图如下:
其中:
- AbstractClass 就是模板方法的抽象类,定义了一个模板方法,该方法规定了执行某一流程的规定步骤。
- PrimitiveOperation* 是子类必须要实现的步骤。
优缺点与应用场景
优点
1. 良好的封装性。把公有的不变的方法封装在父类,而子类负责实现具体逻辑。
2. 良好的扩展性:增加功能由子类实现基本方法扩展,符合单一职责原则和开闭原则。
3. 复用代码。
复制代码
缺点
1. 由于是通过继承实现代码复用来改变算法,灵活度会降低。
2. 子类的执行影响父类的结果,增加代码阅读难度。
复制代码
应用场景
1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现;
2. 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复;
3. 控制子类的扩展。
复制代码
总结与思考
模式总结
模板方法模式是非常常见的模式,常见到我们常常忽略它的存在。模板方法模式本身有许多种实现,比如在模板方法执行的前后加入钩子:preActionHook() 和 postActionHook() ,方便子类在模板方法执行之前或者执行后做一些动作。
模板模式本身比较简单,难的是如何将要做的事情抽象出固定的、与业务无关的步骤。并不是所有的业务都像将大象放入冰箱如此简单,如果稍微复杂一点的业务很多时候我们很难跳出细节看到全貌。当然,学会抽象不是一朝一夕的事情,更多的是要学习、模仿和沉淀。
思考感悟
看懂设计模式 UML 图不是目的,学会将设计模式应用于实际才是。我觉得学习设计模式就像学习功夫,有两种境界:
第一种是当我们不知道任何设计模式的时候,我们冥冥之中也会用。这时候我们就需要学习、刻意的模仿,将自己知道/不知道的设计方法都往设计模式上套。这一种是记住设计模式。
第二种是当我们知道所有设计模式的时候,我们不再刻意的将设计方法套到各种设计模式上,也能写出高内聚低耦合的代码,做到手中无剑心中有剑。这一种是忘记设计模式。
目前我还处在第一种境界中苦苦挣扎,希望各位和我在学习设计模式乃至其他知识都能达到第二种境界。
扩展(可跳过)
实战经验
最近在负责内部门户网站消息的推送系统,需要做各种推送:日推/周推/自定义推/事件推等,经过几次思考和重构将推送流程简化为以下几个步骤:
- 收集需要推送的内容,放入推送模型。
- 将推送模型与 velocity 模板集合。
- 获取接收推送的人信息。
- 调用底层通道发送消息推送(站内信/邮件/钉钉等)。
这种场景下可以定义一个模板方法模型的抽象类:
class AbstractPush {
protected void collectModelData();
protected void generateViewContent();
protected void collectReceivers();
//无需子类实现
private void sendByChannel() {
}
public final void push() {
//通用固定模板代码
collectModelData();
generateViewContent();
collectReceivers();
sendByChannel();
}
}
复制代码
接着定义子类,并在在 client 中调用子类 push 方法即可。只要知道将推送过程简化为以上几个步骤,套用模板方法模式便水到渠成了。
有限状态机模式
模板方法模式中的模板方法往往步骤是固定的且模板方法也是被 final 修饰的。结合有限状态机的定义,如果:
- 模板类中定义起始状态和结束状态,其他状态由子类来控制。
- 模板方法中的每个步骤(除了开始和结束)的扭转由子类的状态来控制。
- 模板方法中的步骤数量在子类中可以控制。
那模板方法模式就转变成了有限状态机模式,伪代码如下:
public class AbstractStateachine() {
protected Integer state;
protected void onStart();
protected abstract void onAction();
protected void onEnd();
protected final onRun() {
switch(state) {
case START:
onStart();
this.state = ACTION;
break;
case END:
onEnd();
break;
default:
onAction();
break;
}
}
}
复制代码
这样只要在子类中定义更多的状态,同时重写 onAction() 方法即可,可能如下:
public class MyStateMachine() {
//...其他省略
protectd void onAction() {
case CUSTOM_STATE_1:
onCustomState1Action();
this.state = CUSTOM_STATE_2;
break;
case CUSTOM_STATE_2:
onCustomState1Action();
this.state = END;
break;
default:
this.state = END;
break;
}
private onCustomState1Action() {
//balabala1
}
private onCustomState2Action(){
//balabala2
}
}
复制代码
当然,以上只是我的一个简略的想法,有错误的地方还请指正。后面还会对有限状态机模式做一个系统的学习和整理。