Template Pattern
认识模板方法
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
案例
【Head First】案例UML图
template-pattern-class-diagram.png
【Head First】案例代码
关于代码的注释和说明,放在对应的代码里面更方便理解。
- CaffeineBeverage.java
/**
* 咖啡因饮料
*
*/
public abstract class CaffeineBeverage {
/**
* 定义制作咖啡因饮料的算法步骤:把水煮沸 -> 冲泡 -> 把饮料倒进杯子 -> 加调料 算法的每一个步骤都被一个方法代表了。
* brew()和addCondiments()方法需要子类来提供实现。
*/
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
/**
* 冲泡
*/
abstract void brew();
/**
* 加调料
*/
abstract void addCondiments();
void boilWater() {
System.out.println("把水煮沸");
}
void pourInCup() {
System.out.println("把饮料倒进杯子");
}
/**
* 钩子方法 可以让子类有能力对算法的 不同点进行挂钩。
* 钩子的目的:子类实现算法的可选的部分;子类能够有机会对模板方法中的某些即将发生的步骤做出反应。
* 通过钩子的控制,子类可以控制是否执行算法里面的某个步骤, 例如饮料是否要加调料,饮料是否需要配送
*/
boolean customerWantsCondiments() {
return true;
}
}
- Tea.java
public class Tea extends CaffeineBeverage {
/**
* 茶在这里作为子类的角色,提供了冲泡步骤的实现
*/
@Override
void brew() {
System.out.println("冲泡茶");
}
/**
* 茶在这里作为子类的角色,提供了添加调料步骤的实现
*/
@Override
void addCondiments() {
System.out.println("添加柠檬调料");
}
}
- Coffe.java
public class Coffee extends CaffeineBeverage {
/**
* 咖啡在这里作为子类的角色,提供了冲泡步骤的实现
*/
@Override
void brew() {
System.out.println("冲泡咖啡");
}
/**
* 咖啡在这里作为子类的角色,提供了添加调料步骤的实现
*/
@Override
void addCondiments() {
System.out.println("添加糖和牛奶");
}
}
JDK AbstractCollection的模板方法
- AbstractCollection.java
public abstract class AbstractCollection<E> implements Collection<E> {
...
// 添加单个元素的算法延迟到了子类实现
public boolean add(E e) {
throw new UnsupportedOperationException();
}
// 添加一个集合的算法步骤
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
...
}
- ArrayList.java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
...
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
...
}
- HashSet.java
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
...
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
...
}
【Spring】JdbcTemplate的UML图
template-pattern-jdbctemplate.png
【Spring】JdbcTemplate主要代码
直接使用JDBC驱动来编写数据库访问代码,算法步骤如下:
1. 注册并加载JDBC驱动程序
2. 建立与数据库的连接
3. 创建Statement对象
4. 执行sql语句
5. 处理sql结果集
6. 异常处理
7. 释放资源
对于数据库的每次访问,都要经过上面7个步骤,写一两次问题不大,现在的程序逻辑一般都要和DB做交互,每次交互都走遍7个步骤,关键是大部分代码都是相同的。
要解决这个问题,运用面向对象的原则,区分不变的步骤和变化的步骤。上面7个步骤里面只有第4个和第5个步骤是会根据业务场景变化的。
Spring将数据访问过程中不变的和变化的部分明确划分为两个不同的类:模板(template)和回调(callback)。模板类管理数据访问过程中不变的部分,例如事务控制,资源管理和异常处理。回调类处理自定义的数据访问代码,例如SQL语句,绑定参数和处理结果集。
+------------------------+ +-----------------------+
| | | |
| 模 板 类 | | 回 调 类 |
| | | |
| +------------------+ | | +------------------+ |
| | | | | | | |
| | | | | | | |
| | 1、准 备 资 源 | | | | | |
| | +--------------> 3、执 行 事 务 | |
| | 2、开 始 事 务 | | | | | |
| | | | | | | |
| | | | | | | |
| +------------------+ | | +------------------+ |
| | | |
| +------------------+ | | +------------------+ |
| | | | | | | |
| | | | | | | |
| | 5、提 交 / 回 滚 | | | | | |
| | <--------------+ 4、返 回 数 据 | |
| | 6、关 闭 资 源 | | | | | |
| | | | | | | |
| | | | | | | |
| +------------------+ | | +------------------+ |
| | | |
+------------------------+ +-----------------------+
- JdbcTemplate.java
/**
* 模板方法的设计模式一般是通过继承来实现,但是这里并没有采用继承让子类承担某个算法的步骤,而是采用了组合委托给另外一个类来负责某个算法的步骤。
* 而组合的方式也不是在JdbcTemplate类中声明固定的变量名,而是通过传参的方式来实现。
*
* queryXXX()和updateXXX()方法最终都转到executeXXX()方法。
* execute()方法就是Template Pattern里面的Template Method,定义了访问数据库的算法骨架:获取数据库连接 -> 创建Statement对象 -> 执行SQL -> 捕获异常 -> 释放资源
* 对于【执行SQL】这个算法步骤,JdbcTemplate委托给StatementCallback对象action来处理。
* 程序员就可以只关注自己的代码逻辑,而不必关注管理资源和异常处理等问题。
*
*/
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
Statement stmt = null;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null &&
this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt = conToUse.createStatement();
applyStatementSettings(stmt);
Statement stmtToUse = stmt;
if (this.nativeJdbcExtractor != null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
T result = action.doInStatement(stmtToUse);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
- StatementCallback子类
StatementCallback子类都是定义在JdbcTemplage.execute()方法里面的内部类,共有4个:QueryStatementCallback,UpdateStatementCallback,ExecuteStatementCallback,BatchUpdateStatementCallback。
下来列举其中两个类的定义:
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
// 查询类的执行需要处理结果集
rs = stmt.executeQuery(sql);
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
public String getSql() {
return sql;
}
}
class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
public Integer doInStatement(Statement stmt) throws SQLException {
// 更新类的可以获取到更新的行数
int rows = stmt.executeUpdate(sql);
if (logger.isDebugEnabled()) {
logger.debug("SQL update affected " + rows + " rows");
}
return rows;
}
public String getSql() {
return sql;
}
}
总结
适用范围
- 「模板方法」定义了算法的步骤,把某些步骤的实现延迟到子类
- 模板方法模式为我们提供了一种代码复用的重要技巧
- 模板方法的抽象类可以定义具体方法、抽象方法和钩子
- 抽象方法由子类实现
- 钩子是一个方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要覆盖它
- 为了防止子类改变模板方法中的算法,可以将模板方法声明为final
- 模板方法的实践应用中有可能会有很多变化
- 工厂方法是模板方法的一种特殊版本
与策略模式的比较
- 两个模式都是封装算法,一个用组合,一个用继承。
体现的OO原则
Don't call me, I will call you.
好莱坞原则:将决策权放在高层模块中,以便决定如何以及何时调用底层模块。