模板模式
如果说“用到了接口-实现类,就用到了策略模式”,那么策略模式可能是我们无意之间用得最多的设计模式。如果排除“无意中”的使用,只考虑有意识、有目的情况,我想,模板模式应该算得上“使用最多的设计模式”,应该也是大多数人第一个运用到实践中的设计模式。
有意和无意,有很大差别。
我想,其中的主要原因在于我们大多数人开发的都是业务系统、业务流程。业务系统和流程有一个显著特点:在一套基础业务流程上,改一改这一步、调一调那一步,就得到了所谓新业务、新流程、新需求。模板模式与这一特点一拍即合,自然就很容易进入到开发实践中。
是什么
关于模板模式的定义,我在网上查到的了很多这样描述的:
模板模式在一个抽象类中定义了一个算法的骨架,而将一些步骤延迟到子类中。
这个定义里,其它部分都好说,就是“延迟”二字让人费解。延迟一般是指“比xxx更慢/更晚”。在模板模式里,是谁比谁更慢或更晚呢?
看了一圈,我比较喜欢这个定义:
The Template Method design pattern is a behavioral design pattern that defines the skeleton of an algorithm in a superclass but allows subclasses to override specific steps of the algorithm without changing its structure. 模板方法模式是一种行为设计模式。它在父类中好定义算法骨架,并允许子类在不修改算法结构的情况下,重写其中的特定的步骤。
从这个定义中,我们可以找出模板模式的四个关键要素:父类、子类、算法骨架、被重写的步骤,如下图所示:
父类之于子类有很多种意义。在模板模式中,父类的主要意义在于定义算法骨架。
所谓“算法骨架”,首先是操作流程,其次则是“可重写的步骤”。有了“可重写的步骤”,子类才能够“重写”这些步骤。但也因为操作流程在父类中定义好了,因而子类只能重写个别步骤的具体实现,而不能修改算法的完整流程、步骤顺序等。
网上提到模板模式,大多强调父类一定是抽象类。其实未必。尽管不太符合里氏替换原则,模板模式中的父类也可以是普通类。这是后话。
所谓“可重写步骤”,通常即封装有部分操作流程的方法。有些时候,它们也叫“钩子方法”。子类通过重写这些方法,来改写操作流程中的部分细节。这些方法的可见性、入参、返回值,以及“颗粒度”等,都是值得慎重考虑的方面。
在这些方面中,可见性也许是其中最简单的一个。为了兼容跨包的子类,父类的可见性一般都是public。但方法的可见性则不宜过大。有些代码里一股脑地将它们也定义为public,也有些开发者把这些方法放到了接口上,实在没有必要。
这些方法只服务于两个目标:组成完整的操作流程,以及供子类重写。因此,保证这些方法能被子类访问到即可,其它调用者一律禁止入内。
一个方法的可扩展性,受方法入参影响很大。我见过不少子类A要用参数ABC、而子类B要用参数CDE的。如果方法入参定义不当,从子类一扩展到子类二时,就免不了一番伤筋动骨。
怎么做
// 父类,定义算法的骨架
public class ParentClass {
// 定义算法的步骤
public final Result templateMethod(Param p) {
Temp1 temp1 = step1(p);
Temp2 temp2 = step2(p, temp1);
Result result = step3(temp1, temp2);
return result;
}
// 定义算法的第一步
protected Temp1 step1(Param p){
// 默认实现,略
}
// 定义算法的第二步
protected Temp2 step2(Param p, Temp1 temp1){
// 默认实现,略
}
// 定义算法的第三步
protected Result step3(Temp1 temp1,Temp2 temp2){
// 默认实现,略
}
}
// 具体子类,实现算法的步骤
class ConcreteClassA extends ParentClass {
@Override
protected Temp1 step1(Param p){
// 修改实现步骤1,略
}
@Override
protected Result step3(Temp1 temp1,Temp2 temp2){
// 修改实现步骤3,略
}
}
// 具体子类,实现算法的步骤
class ConcreteClassB extends ParentClass {
@Override
protected Temp2 step2(Param p, Temp1 temp1){
// 修改实现步骤2,略
}
}
// 使用模板模式
public class Client {
public static void main(String[] args) {
ParentClass template = new ConcreteClassA();
template.templateMethod();
template = new ConcreteClassB();
template.templateMethod();
}
}
一般来说,拆分方法步骤有两种思路。第一种是在还没有子类的时候,就在父类中设计好方法步骤,并预留下供子类重写的方法。第二种则是没有子类时,父类不做扩展性考虑;在必要时,再根据子类的扩展需求,在父类中拆分方法步骤、设计可重写方法。
这两种方法,和“自上而下”和“自下而上”的方法论颇有些异曲同工。它们的优缺点也可以从类似的角度来分析。
第一种方法,“自上而下”地设计好一个模板类,为子类预留下扩展空间。如果父类的流程有明确的步骤,或者子类的扩展有清晰的方向,那么借助这种自上而下的方法,我们可以设计出一套卓有成效的父子类来。
然而,如果没有这两个前提——“父类的流程有明确的步骤”,或者“子类的扩展有清晰的方向”——自上而下的方法就很容易变成过度设计,最终得到南辕北辙的效果。
第二种方法则是“自下而上”地演化出一套模板和父子类。它很好地弥补了第一种方法的缺陷,但也有明显的问题。
“按需演化”很容易演变成“按需特化”,得到的模板不伦不类,既复杂难懂、又难以扩展。
我个人更倾向于从上而下的方法论。条件允许时,应该优先考虑自上而下地设计,用优秀的“顶层设计”来指导、推动底层实现。如果从自下而上入手,也应注意审时度势,在必要的时候梳理顶层设计,优化模板类结构。
无论自上而下还是自下而上,都是一件费时费心的工作。我们为什么要花费时间精力来设计和使用模板模式呢?模板模式有怎样的优点,又有哪些缺点呢?
为什么
模板模式可以提高代码复用率和可扩展性。这大概是它最显而易见的两个优点,这里就不啰嗦了。
模板模式还有一个更高层次的优点:它是一种入门级的建模方式。
个人理解,建模需要抓住事物的核心本质,并简练而明确地描述其组成元素和各元素的组织结构、运转规律。
设计模板模式正是这样一个过程。我们先要分析业务流程,然后把流程按一定顺序分解成若干步骤,识别各步骤的入参和返回值,还要把这些步骤区分为“所有子类通用的”和“允许子类修改的”。完成这些工作之后,我们才能确定父类如何实现、子类如何重写。
在这个过程中,我们得到的步骤、入参和返回值,其实就是这个流程模型中的“元素”;各步骤的执行顺序、入参和返回值的转换和传递,就是这个模型的运转规律;步骤的分类则可以理解为这个模型的组织结构的基础:我们需要借助“允许子类修改的步骤”,来组织父子类结构。
你看,一个流程模型就这样拔地而起了。
当然,建模没有这么简单, 建好模更不容易。不过,用模型思维来分析和构建系统,于系统、于自己都百利一弊——唯一的弊端在于成本偏高。
模板模式作为一种入门级的建模方式,就像是一扇通往模型思维的大门。当我们有意识、有目的地使用模板模式时,其实就已经站到了这扇门前。推开它、走进去,我们会发现更广阔的天地。
推开大门和掉头走开,有很大差别。
模型的7大用途(REDCAPE)
推理:识别条件并推断逻辑含义。
解释:为经验现象提供(可检验的)解释。
设计:选择制度、政策和规则的特征。
沟通:将知识与理解联系起来。
行动:指导政策选择和战略行动。
预测:对未来和未知现象进行数值和分类预测。
探索:分析探索可能性和假说。
《模型思维》
总的来说,模板模式在需要重用业务流程、同时允许子类重写某些步骤的场景中非常有用。
然而,在选择使用模板模式时,我们需要权衡其带来的代码复用和行为一致性的优点,以及可能增加的类数量和复杂性的缺点,审慎使用。