Template Method Design Pattern
GoF 的定义: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 => B => C ... 的骨架父类,假设 A 是重复不变的,就在父类中实现,将可变的步骤留给子类实现。从而将独立变化的部分抽象出来。
假设有一个回家过年的流程:买票、坐车、回家。
一开始只支持火车,需要买火车票、坐火车、回到家。写一个程序模拟这个过程就是:
func goHome() {
// buyTrainTicket
...
// travelByTrain
...
// arriveHome
...
}
后续业务拓展,回家过年不仅可以选择火车,还可以选择大巴、飞机、游艇....(需求变化),但是不管哪种方式都得先买票,乘坐,然后到家(不变的流程)。
如果为了新增的方式,我们把 goHome 复制一份,改改变成 goHomeByAriplane、goHomeByCoach...,买票和乘坐都要改写,由于到家的方式一样,arriveHome 部分不用修改,但也被复制粘贴了很多份(不变的部分)。
如果到家的方式有变化,就需要修改三个地方,如果工程师遗漏了一个地方就会有问题(代码的坏味道,重复修改),而且如果这三个方法有单元测试,也要修改三个地方。
最后,如果交通方式越来越多,需要复制粘贴更多的方法,这样维护就变得越来越麻烦,也明显的违反了 DRY 原则。
在软件开发过程中,时刻发生着变化,特别是需求变化,如果像这样编写重复的代码,维护这些代码会给我们带来噩梦。需求变化一次要修改 N 个一样的地方,随着不断地开发维护,相同功能的实现在不同地方会变得不尽相同,有时甚至出现相同的需求实现逻辑却不相同的,由于实现的不同,导致这些代码的健壮性也差异很大,很难维护。也不利于测试,会导致测试代码重复开发,不利于阅读和维护、影响性能等问题。
怎么消除重复呢?可以使用 OOP 的一大特征:继承
把回家的核心流程抽象到父类中,父类实现不变的 arriveHome,每个不同的交通方式继承父类,不需要实现不变的 arriveHome 避免了代码重复,只需要实现变化的 buyTicket 和 travel 即可:
class GoHome() {
buyTicket()
travel()
arriveHome()
goHome() {
buyTicket()
travel()
arriveHome()
}
}
这就是所谓的模板模式。把相似的流程分成多步,抽象隔离出变化和不变的部分作为方法,变化的部分使用子类实现,从而解决代码冗余问题,也能实现 OCP,新增一个交通方式只需要扩展一个新的类,不需要修改原来的代码。
父类提供了算法的框架,控制方法执行流程,而子类不能改变算法流程,子类方法的调用由父类模板方法决定。执行步骤的顺序有时候非常重要,我们在容器加载和初始化资源时,为避免子类执行错误的顺序,经常使用该模式限定子类方法的调用次序。比如此例中,你不能先回家再买票。
父类可以把那些不允许改变的方法屏蔽掉,不让子类去覆写(Override/Overwrite)它们,比如在 Java 中声明这些方法为 final 或 private。
在业务中还有很多这种场景,例如支付渠道有很多微信、支付宝、银联,但流程类似,其中也有很多不变的地方。
子类的泛滥问题
模板模式解决了某些场景中的代码重复问题,但过度使用也会导致子类的泛滥,可维护性下降。
例如查询数据库记录有三步:
- 获取一个数据库连接
- 创建 Statement 实例并执行查询语句
- 处理查询到的结果,并处理异常
在不同的查询中,第一步、第二步和处理异常都是相同的,只有对查询结果的处理不同,很适合使用模板方法,只需要抽象出查询结构处理方法给子类实现就行。
但是,由于各种各样的查询很多,慢慢地就会积累一堆子类处理不同的查询结果,子类就泛滥了,也很难维护。
怎么办?
减少类的数量,把变化的处理查询结果逻辑通过回调函数传进去,通过这种方式把过多的变化从模板方法中抽离出来。
import java.sql.ResultSet
public class SimpleJdbcQueryTemplate {
public <T> T query(String queryString, ResultSetHandler<T> rsHandler)
try {
connection = getConnection()
stmt = connection.prepareStatement(queryString)
ResultSet rs = stmt.executeQuery()
return rsHandler.handle(rs)
} catch (SQLException ex) {
// handle exceptions...
} finally {
// close statement and connection...
}
}
public interface ResultSetHandler<T> {
public T handle(ResultSet rs)
}
// using
ret = new SimpleJdbcQueryTemplate().query("select * from db", new ResultSetHandler<Boolean>(){
public Boolean handle(ResultSet rs) {
return Boolean.TRUE;
}
}) // ret == true
将变化抽离到回调函数中,此时只需要一个模板类封装查询流程就行了,使用的时候动态传入回调函数处理返回结果即可。
使用回调可以避免类的泛滥,可以在某个变化点特别多的时候使用,但也不能用太多,如果回调实现的接口较多,代码较为复杂时,把这些代码挤在一起也会引起阅读问题。注意 Trade Off。
局限性
如果需求变得更为复杂,模板方法模式可能就就不够用了,此时需要更灵活的设计,比如策略模式。
WHY
为什么需要模板方法模式? 本质上是为了消除重复代码,通过继承的方式将相似的业务流程整合到一个模板类中,区分出流程中变化和不变的部分,每个子类实现变化的部分,复用不变的部分,可以有效的管理不同的业务逻辑。 如果某个变化点太多,例如数据库查询结果的处理,可以把变化完全抽出来,变成一个回调函数传进去,也是妙哉。