設計模式 | 模板方法模式及典型應用

小旋鋒發表於2018-10-10

本文的主要內容:

  • 介紹模板方法模式
  • 原始碼分析模板方法模式的典型應用
    • Servlet 中的模板方法模式
    • Mybatis BaseExecutor介面中的模板方法模式

更多內容可訪問我的個人部落格:laijianfeng.org
歡迎關注【小旋鋒】微信公眾號,及時接收博文推送

關注【小旋鋒】微信公眾號


模板方法模式

在程式開發中,經常會遇到這種情況:某個方法要實現的演算法需要多個步驟,但其中有一些步驟是固定不變的,而另一些步驟則是不固定的。為了提高程式碼的可擴充套件性和可維護性,模板方法模式在這種場景下就派上了用場。

譬如製作一節網課的步驟可以簡化為4個步驟:

  1. 製作PPT
  2. 錄製視訊
  3. 編寫筆記
  4. 提供課程資料

其中1、2、3的動作在所有課程中的固定不變的,步驟3可有可無,步驟4在每個課程都不同(有些課程需要提供原始碼,有些需要提供圖片檔案等)

我們可以在父類中確定整個流程的循序,並實現固定不變的步驟,而把不固定的步驟留給子類實現。甚至可以通過一個鉤子方法,讓子類來決定流程中某個方法的執行與否

示例.模板方法模式

模板方法模式:定義一個操作中演算法的框架,而將一些步驟延遲到子類中。模板方法模式使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。模板方法模式是一種基於繼承的程式碼複用技術,它是一種類行為型模式。

角色

AbstractClass(抽象類):在抽象類中定義了一系列基本操作(PrimitiveOperations),這些基本操作可以是具體的,也可以是抽象的,每一個基本操作對應演算法的一個步驟,在其子類中可以重定義或實現這些步驟。同時,在抽象類中實現了一個模板方法(Template Method),用於定義一個演算法的框架,模板方法不僅可以呼叫在抽象類中實現的基本方法,也可以呼叫在抽象類的子類中實現的基本方法,還可以呼叫其他物件中的方法。

ConcreteClass(具體子類):它是抽象類的子類,用於實現在父類中宣告的抽象基本操作以完成子類特定演算法的步驟,也可以覆蓋在父類中已經實現的具體基本操作。

一個模板方法是定義在抽象類中的、把基本操作方法組合在一起形成一個總演算法或一個總行為的方法。這個模板方法定義在抽象類中,並由子類不加以修改地完全繼承下來。模板方法是一個具體方法,它給出了一個頂層邏輯框架,而邏輯的組成步驟在抽象類中可以是具體方法,也可以是抽象方法。

基本方法是實現演算法各個步驟的方法,是模板方法的組成部分。基本方法又可以分為三種:抽象方法(Abstract Method)、具體方法(Concrete Method)和鉤子方法(Hook Method)。

  • 抽象方法:一個抽象方法由抽象類宣告、由其具體子類實現。
  • 具體方法:一個具體方法由一個抽象類或具體類宣告並實現,其子類可以進行覆蓋也可以直接繼承。
  • 鉤子方法:可以與一些具體步驟 "掛鉤" ,以實現在不同條件下執行模板方法中的不同步驟

示例

所以我們通過模板方法模式,在抽象類中把整個流程固定下來,其中1、2、3的實現在抽象類中完成,3的執行與否則由子類通過鉤子方法來控制,4則由子類來實現

抽象類定義如下:

public abstract class ACourse {

    protected final void makeCourse() {
        this.makePPT();
        this.makeVideo();
        if (needWriteArticle()) {
            this.writeArticle();
        }
        this.packageCourse();
    }

    final void makePPT() {
        System.out.println("1. 製作PPT");
    }

    final void makeVideo() {
        System.out.println("2. 製作視訊");
    }

    final void writeArticle() {
        System.out.println("3. 編寫課程筆記");
    }

    //鉤子方法
    protected boolean needWriteArticle() {
        return false;
    }

    abstract void packageCourse();
}
複製程式碼

其中的 makeCourse 方法是模板方法,它定義了製作網課的基本流程,makePPTmakeVideowriteArticle 這三個步驟在所有課程中都是固定的,所以用 final 關鍵字修飾;packageCourse 方法在所有課程中都可能不一樣,所以宣告為抽象方法,由子類自行實現;鉤子方法 needWriteArticle 返回一個 boolean 型別的值,控制是否編寫課程筆記

子類 JavaCourse,實現了抽象方法 packageCourse,重寫了鉤子方法 needWriteArticle

public class JavaCourse extends ACourse {
    @Override
    void packageCourse() {
        System.out.println("4. 提供Java課程原始碼");
    }

    @Override
    protected boolean needWriteArticle() {
        return true;
    }
}
複製程式碼

子類 FECourse,實現了抽象方法 packageCourse,重寫了鉤子方法 needWriteArticle,其中把鉤子方法的結果交給客戶端確定

public class FECourse extends ACourse {
    private boolean needWriteArticleFlag = false;
    @Override
    void packageCourse() {
        System.out.println("4.1 提供課程的前端程式碼");
        System.out.println("4.2 提供課程的圖片等多媒體素材");
    }

    public FECourse(boolean needWriteArticleFlag) {
        this.needWriteArticleFlag = needWriteArticleFlag;
    }

    @Override
    protected boolean needWriteArticle() {
        return this.needWriteArticleFlag;
    }
}
複製程式碼

客戶端測試

public class Test {
    public static void main(String[] args) {
        System.out.println("Java課程start---");
        ACourse javaCourse = new JavaCourse();
        javaCourse.makeCourse();
        System.out.println("Java課程end---\n");


        System.out.println("前端課程start---");
        ACourse feCourse = new FECourse(false);
        feCourse.makeCourse();
        System.out.println("前端課程end---");
    }
}
複製程式碼

輸出結果

Java課程start---
1. 製作PPT
2. 製作視訊
3. 編寫筆記
4. 提供Java課程原始碼
Java課程end---

前端課程start---
1. 製作PPT
2. 製作視訊
4.1 提供課程的前端程式碼
4.2 提供課程的圖片等多媒體素材
前端課程end---
複製程式碼

它們的類圖如下

示例.模板方法模式

模板方法模式總結

模板方法模式的主要優點如下:

  • 在父類中形式化地定義一個演算法,而由它的子類來實現細節的處理,在子類實現詳細的處理演算法時並不會改變演算法中步驟的執行次序。
  • 模板方法模式是一種程式碼複用技術,它在類庫設計中尤為重要,它提取了類庫中的公共行為,將公共行為放在父類中,而通過其子類來實現不同的行為,它鼓勵我們恰當使用繼承來實現程式碼複用。
  • 可實現一種反向控制結構,通過子類覆蓋父類的鉤子方法來決定某一特定步驟是否需要執行。
  • 在模板方法模式中可以通過子類來覆蓋父類的基本方法,不同的子類可以提供基本方法的不同實現,更換和增加新的子類很方便,符合單一職責原則和開閉原則。

模板方法模式的主要缺點如下:

  • 需要為每一個基本方法的不同實現提供一個子類,如果父類中可變的基本方法太多,將會導致類的個數增加,系統更加龐大,設計也更加抽象,此時,可結合橋接模式來進行設計。

適用場景

  • 對一些複雜的演算法進行分割,將其演算法中固定不變的部分設計為模板方法和父類具體方法,而一些可以改變的細節由其子類來實現。即:一次性實現一個演算法的不變部分,並將可變的行為留給子類來實現。
  • 各子類中公共的行為應被提取出來並集中到一個公共父類中以避免程式碼重複。
  • 需要通過子類來決定父類演算法中某個步驟是否執行,實現子類對父類的反向控制。

原始碼分析模板方法模式的典型應用

Servlet 中的模板方法模式

Servlet(Server Applet)是Java Servlet的簡稱,用Java編寫的伺服器端程式,主要功能在於互動式地瀏覽和修改資料,生成動態Web內容。在每一個 Servlet 都必須要實現 Servlet 介面,GenericServlet 是個通用的、不特定於任何協議的Servlet,它實現了 Servlet 介面,而 HttpServlet 繼承於 GenericServlet,實現了 Servlet 介面,為 Servlet 介面提供了處理HTTP協議的通用實現,所以我們定義的 Servlet 只需要繼承 HttpServlet 即可。

HttpServlet的繼承關係

HttpServlet 的簡要程式碼如下所示

public abstract class HttpServlet extends GenericServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doDelete(HttpServletRequest req,  HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
            
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
            
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
            
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
            
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
            
        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
    
    // ...省略...
}
複製程式碼

HttpServletservice 方法中,首先獲得到請求的方法名,然後根據方法名呼叫對應的 doXXX 方法,比如說請求方法為GET,那麼就去呼叫 doGet 方法;請求方法為POST,那麼就去呼叫 doPost 方法

HttpServlet 相當於定義了一套處理 HTTP 請求的模板;service 方法為模板方法,定義了處理HTTP請求的基本流程;doXXX 等方法為基本方法,根據請求方法做相應的處理,子類可重寫這些方法;HttpServletRequest 中的Method則起到鉤子方法的作用.

在開發javaWeb應用時,自定義的Servlet類一般都擴充套件 HttpServlet 類,譬如我們實現一個輸出 Hello World!Servlet 如下

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

// 擴充套件 HttpServlet 類
public class HelloWorld extends HttpServlet {

  public void init() throws ServletException {
    // ...
  }

  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.setContentType("text/html");
      PrintWriter out = response.getWriter();
      out.println("<h1>Hello World!</h1>");
  }
  
  public void destroy() {
      // ...
  }
}
複製程式碼

該自定義的 Servlet 重寫了 doGet 方法,當客戶端發起 GET 請求時將得到 <h1>Hello World!</h1>

Mybatis BaseExecutor介面中的模板方法模式

Executor 是 Mybatis 的核心介面之一,其中定義了資料庫操作的基本方法,該介面的程式碼如下:

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;
  // 執行 update、insert、delete 三種型別的SQL語句
  int update(MappedStatement ms, Object parameter) throws SQLException;
  // 執行selete型別的SQL語句,返回值分為結果物件列表或遊標物件
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
  /
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; 
  // 批量執行SQL語句
  List<BatchResult> flushStatements() throws SQLException;
  // 提交事務
  void commit(boolean required) throws SQLException;
  // 回滾事務
  void rollback(boolean required) throws SQLException;
  // 建立快取中用到的CacheKey物件
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
  // 根據CacheKey物件查詢快取
  boolean isCached(MappedStatement ms, CacheKey key);
  // 清空一級快取
  void clearLocalCache();
  // 延遲載入一級快取中的資料
  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
  // 獲取事務物件
  Transaction getTransaction();
  // 關閉Executor物件
  void close(boolean forceRollback);
  // 檢測Executor是否已關閉
  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}
複製程式碼

Executor 類的類圖如下

Executor與其子類的類圖

BaseExecutor 中主要提供了快取管理和事務管理的基本功能,繼承 BaseExecutor 的子類只需要實現四個基本方法來完成資料庫的相關操作即可,這四個方法分別是:doUpdate() 方法、doQuery() 方法、doQueryCursor() 方法、doFlushStatement() 方法,其餘功能都在 BaseExecutor 中實現。

BaseExecutor的部分程式碼如下,其中的 query() 方法首先會建立 CacheKey 物件,並根據 CacheKey 物件查詢一級快取,如果快取命中則返回快取中記錄的結果物件,如果未命中則查詢資料庫得到結果集,之後將結果集對映成結果物件並儲存到一級快取中,同時返回結果物件。

public abstract class BaseExecutor implements Executor {
  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack = 0;
  private boolean closed;
  
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
  
  protected abstract int doUpdate(MappedStatement ms, Object parameter)
      throws SQLException;

  protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
      throws SQLException;

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

  protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException;

  // 省略....
}
複製程式碼

BaseExecutor 的子類有四個分別是 SimpleExecotorReuseExecutorBatchExecutorClosedExecutor,由於這裡使用了模板方法模式,一級快取等固定不變的操作都封裝到了 BaseExecutor 中,因此子類就不必再關心一級快取等操作,只需要專注實現4個基本方法的實現即可。

這裡對這四個子類的功能做一個簡要的介紹:

  • SimpleExecutor 是Mybatis執行Mapper語句時預設使用的 Executor,提供最基本的Mapper語句執行功能,沒有過多的封裝的
  • ReuseExecutor 提供了 Statement 重用的功能,通過 statementMap 欄位快取使用過的 Statement 物件進行重用,可以減少SQL預編譯以及建立和銷燬 Statement 物件的開銷,從而提高效能
  • BatchExecutor 實現了批處理多條SQL語句的功能,在客戶端快取多條SQL並在合適的時機將多條SQL打包傳送給資料庫執行,從而減少網路方面的開銷,提升系統的效能
  • ClosedExecutor 只是某個類的一個內部類

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析
徐郡明:Mybatis技術內幕 3.6 Executor

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用
設計模式 | 外觀模式及典型應用
設計模式 | 裝飾者模式及典型應用
設計模式 | 介面卡模式及典型應用
設計模式 | 享元模式及典型應用
設計模式 | 組合模式及典型應用

相關文章