设计模式-模板方法

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.

好莱坞原则:将决策权放在高层模块中,以便决定如何以及何时调用底层模块。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容