從源碼學習設計模式之模板方法

Photo by Tomáš Malík on Unsplash

什麼是模板方法模式?摘錄 wiki 的介紹。

模板方法模式定義了一個算法的步驟,並允許子類別爲一個或多個步驟提供其實踐方式。讓子類別在不改變算法架構的情況下,重新定義算法中的某些步驟。在軟件工程中,它是一種軟件設計模式,和C++模板沒有關連。

模板設計方法存在目的在於某些算法邏輯存在一些相同處,而具體細節卻不同。這樣使用模板方法,可以抽取共用邏輯到父類,在子類實現具體算法細節,這樣減少了重複代碼。 模板方法充分運用了多態與繼承。使用抽象父類定義抽象操作,然後在公共邏輯調用抽象方法。子類方法只要繼承父類關注自身實現細節。

Talk is cheap. Show me the code

下面拿支付接入支付渠道例子來使用模板方法。

假設銀行卡支付需要實現兩家銀行的支付功能。不同銀行提供的接口,在參數,調用方式等肯定存在很大區別。這個時候我們就可以使用模板設計方法,父類實現支付前通用邏輯,用子類實現交互的不同。系統類結構如下。

系統類結構
AgreementPay 提供支付功能,AgreementBasePay 爲抽象類實現通用邏輯,AgreementCCBPay 與 AgreementCMBPay 實現具體的渠道支付方法。具體源碼如下。

AgreementPay 接口

public interface AgreementPay {

PayResponse payInChannel(PayReauest reauest);

}

AgreementBasePay 抽象方法實現通用邏輯。

public abstract class AgreementBasePay implements AgreementPay {

public PayResponse pay(PayReauest reauest) {
    checkRequest(reauest);
    return this.payInChannel(reauest);
}


private void checkRequest(PayReauest reauest) {
    System.out.println("具體方法參數檢查");
}

}
具體實現類,實現具體渠道支付細節。

public class AgreementCCBPay extends AgreementBasePay {

@Override
public PayResponse payInChannel(PayReauest reauest) {
    System.out.println("去建設銀行支付");
    return new PayResponse();
}

}

public class AgreementCMBPay extends AgreementBasePay {

@Override
public PayResponse payInChannel(PayReauest reauest) {
    System.out.println("去招商銀行支付");
    return new PayResponse();
}

}

實現模板方法的細節,我們來看 client 使用邏輯。

public class Client {

public static void main(String[] args) {
    System.out.println("使用招商銀行支付");
    AgreementPay agreementPay = new AgreementCMBPay();
    PayRequest request = new PayRequest();
    agreementPay.payInChannel(request);
    System.out.println("使用建設銀行支付");
    agreementPay = new AgreementCCBPay();
    agreementPay.payInChannel(request);
}

}

上面 client 邏輯,其實看起來還是有一些死板,且需要外部知道調用哪個渠道接口。但是如果真正提供一個對外接口,外部調用方法是不關心你具體使用那個子類支付。所以這裏我們可以改進一下,

public static Map<String, AgreementPay> payCache = new HashMap<>();

static {
    payCache.put("CMB", new AgreementCMBPay());
    payCache.put("CCB", new AgreementCCBPay());
}


public static void main(String[] args) {
    PayRequest request = new PayRequest();
    AgreementPay pa;
    switch (request.getBankCode()) {
        case "CMB":
            pa = payCache.get("CMB");
            pa.payInChannel(request);
        case "CCB":
            pa = payCache.get("CCB");
            pa.payInChannel(request);
        default:
            throw new RuntimeException();
    }

}

改造之後我們先將其 AgreementPay 實例放入 map 中,然後調用時根據一個標誌來選擇具體實現類。

從上面的細節我們可以看到模板方法其實設計思路與實現細節都比較簡單。看完我們的示例代碼,我們去看下 mybatis 如何使用模板方法。

mybatis 模板方法應用
在看源碼之前,我們先看下我們不使用 mybatis 之前,如何查詢數據。

    Class.forName("com.mysql.jdbc.Driver");
    //2.獲得數據庫的連接
    Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD);
    //3.通過數據庫的連接操作數據庫,實現增刪改查
    PreparedStatement pstmt = conn.prepareStatement("select user_name,age from imooc_goddess where id=?");
    pstmt.setInt(1, 21);
    ResultSet rs = pstmt.execute();
    
    while (rs.next()) {//如果對象中有數據,就會循環打印出來
        System.out.println(rs.getString("user_name") + "," + rs.getInt("age"));
    }

我們可以看到直接使用 JDBC 查詢,十分麻煩,且需要我們自己將 java 類型轉換成 jdbc 數據類型。

ORM 框架重要作用在於把數據庫表與 java,ORM 框架省去我們自己將 java 類型轉化成 JDBC 類型的麻煩。JDBC 存在有那麼多類型,如何做到轉換的那?其實關鍵就是應用模板設計方法。

mybatis 中存在一個接口 TypeHandler,該接口方法主要如下:

public interface TypeHandler<T> {

void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

T getResult(ResultSet rs, String columnName) throws SQLException;

T getResult(ResultSet rs, int columnIndex) throws SQLException;

T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}
從方法上看,這個接口主要的方法爲 PreparedStatement 設置列參數,或者從 ResultSet 獲取列的值然後轉換成相應的 java 數據類型。我們看下這個接口實現的類圖。

TypeHandler 實現類圖
可以看到 BaseTypeHandler 爲 TypeHandler 的具體抽象類,我們具體看下 TypeHandler getResult 在抽象類中實現細節。

@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {

T result;
try {
  result = getNullableResult(rs, columnName);
} catch (Exception e) {
  throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
}
if (rs.wasNull()) {
  return null;
} else {
  return result;
}

}

public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

可以看到其最後調用抽象方法 getNullableResult。其由具體的子類的實現。我們具體找一個子類 DateTypeHandler 來查看具體實現。

public class DateTypeHandler extends BaseTypeHandler<Date> {

// 忽略其他方法

@Override
public Date getNullableResult(ResultSet rs, String columnName)

  throws SQLException {
Timestamp sqlTimestamp = rs.getTimestamp(columnName);
if (sqlTimestamp != null) {
  return new Date(sqlTimestamp.getTime());
}
return null;

}

}

可見其具體從 ResultSet 取出 JDBC 類型爲 Timestamp,然後轉換成 java 類型的 Date。

實現具體的子類,那麼在哪裏使用了那?其實 mybatis 框架會把所有 TypeHandler 在 TypeHandlerRegistry 註冊。具體類方法如圖

TypeHandlerRegistry

其提供了相關 register 方法註冊 TypeHandler,然後又提供了相關 getTypeHandler 方法取出具體 TypeHandler 實現類。

總結
使用模板方法,將公共邏輯抽取出來,將具體實現細節交給子類。

參考
Mybatis源代碼分析之類型轉換

什麼是模板方法模式?摘錄 wiki 的介紹。

模板方法模式定義了一個算法的步驟,並允許子類別爲一個或多個步驟提供其實踐方式。讓子類別在不改變算法架構的情況下,重新定義算法中的某些步驟。在軟件工程中,它是一種軟件設計模式,和C++模板沒有關連。

模板設計方法存在目的在於某些算法邏輯存在一些相同處,而具體細節卻不同。這樣使用模板方法,可以抽取共用邏輯到父類,在子類實現具體算法細節,這樣減少了重複代碼。
模板方法充分運用了多態與繼承。使用抽象父類定義抽象操作,然後在公共邏輯調用抽象方法。子類方法只要繼承父類關注自身實現細節。

Talk is cheap. Show me the code

下面拿支付接入支付渠道例子來使用模板方法。

假設銀行卡支付需要實現兩家銀行的支付功能。不同銀行提供的接口,在參數,調用方式等肯定存在很大區別。這個時候我們就可以使用模板設計方法,父類實現支付前通用邏輯,用子類實現交互的不同。系統類結構如下。

系統類結構

AgreementPay 提供支付功能,AgreementBasePay 爲抽象類實現通用邏輯,AgreementCCBPayAgreementCMBPay 實現具體的渠道支付方法。具體源碼如下。

AgreementPay 接口


public interface AgreementPay {

    PayResponse payInChannel(PayReauest reauest);
}

AgreementBasePay 抽象方法實現通用邏輯。


public abstract class AgreementBasePay implements AgreementPay {

    public PayResponse pay(PayReauest reauest) {
        checkRequest(reauest);
        return this.payInChannel(reauest);
    }


    private void checkRequest(PayReauest reauest) {
        System.out.println("具體方法參數檢查");
    }
}

具體實現類,實現具體渠道支付細節。


public class AgreementCCBPay extends AgreementBasePay {
    @Override
    public PayResponse payInChannel(PayReauest reauest) {
        System.out.println("去建設銀行支付");
        return new PayResponse();
    }
}

public class AgreementCMBPay extends AgreementBasePay {
    @Override
    public PayResponse payInChannel(PayReauest reauest) {
        System.out.println("去招商銀行支付");
        return new PayResponse();
    }
}

實現模板方法的細節,我們來看 client 使用邏輯。


public class Client {

    public static void main(String[] args) {
        System.out.println("使用招商銀行支付");
        AgreementPay agreementPay = new AgreementCMBPay();
        PayRequest request = new PayRequest();
        agreementPay.payInChannel(request);
        System.out.println("使用建設銀行支付");
        agreementPay = new AgreementCCBPay();
        agreementPay.payInChannel(request);
    }
}

上面 client 邏輯,其實看起來還是有一些死板,且需要外部知道調用哪個渠道接口。但是如果真正提供一個對外接口,外部調用方法是不關心你具體使用那個子類支付。所以這裏我們可以改進一下,


    public static Map<String, AgreementPay> payCache = new HashMap<>();

    static {
        payCache.put("CMB", new AgreementCMBPay());
        payCache.put("CCB", new AgreementCCBPay());
    }


    public static void main(String[] args) {
        PayRequest request = new PayRequest();
        AgreementPay pa;
        switch (request.getBankCode()) {
            case "CMB":
                pa = payCache.get("CMB");
                pa.payInChannel(request);
            case "CCB":
                pa = payCache.get("CCB");
                pa.payInChannel(request);
            default:
                throw new RuntimeException();
        }

    }

改造之後我們先將其 AgreementPay 實例放入 map 中,然後調用時根據一個標誌來選擇具體實現類。

從上面的細節我們可以看到模板方法其實設計思路與實現細節都比較簡單。看完我們的示例代碼,我們去看下 mybatis 如何使用模板方法。

mybatis 模板方法應用

在看源碼之前,我們先看下我們不使用 mybatis 之前,如何查詢數據。


        Class.forName("com.mysql.jdbc.Driver");
        //2.獲得數據庫的連接
        Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD);
        //3.通過數據庫的連接操作數據庫,實現增刪改查
        PreparedStatement pstmt = conn.prepareStatement("select user_name,age from imooc_goddess where id=?");
        pstmt.setInt(1, 21);
        ResultSet rs = pstmt.execute();
        
        while (rs.next()) {//如果對象中有數據,就會循環打印出來
            System.out.println(rs.getString("user_name") + "," + rs.getInt("age"));
        }

我們可以看到直接使用 JDBC 查詢,十分麻煩,且需要我們自己將 java 類型轉換成 jdbc 數據類型。

ORM 框架重要作用在於把數據庫表與 java,ORM 框架省去我們自己將 java 類型轉化成 JDBC 類型的麻煩。JDBC 存在有那麼多類型,如何做到轉換的那?其實關鍵就是應用模板設計方法。

mybatis 中存在一個接口 TypeHandler,該接口方法主要如下:


public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

從方法上看,這個接口主要的方法爲 PreparedStatement 設置列參數,或者從 ResultSet 獲取列的值然後轉換成相應的 java 數據類型。我們看下這個接口實現的類圖。

TypeHandler 實現類圖

可以看到 BaseTypeHandlerTypeHandler 的具體抽象類,我們具體看下 TypeHandler getResult 在抽象類中實現細節。


  @Override
  public T getResult(ResultSet rs, String columnName) throws SQLException {
    T result;
    try {
      result = getNullableResult(rs, columnName);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
    }
    if (rs.wasNull()) {
      return null;
    } else {
      return result;
    }
  }

   public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

可以看到其最後調用抽象方法 getNullableResult。其由具體的子類的實現。我們具體找一個子類 DateTypeHandler 來查看具體實現。


public class DateTypeHandler extends BaseTypeHandler<Date> {


    // 忽略其他方法
  @Override
  public Date getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    Timestamp sqlTimestamp = rs.getTimestamp(columnName);
    if (sqlTimestamp != null) {
      return new Date(sqlTimestamp.getTime());
    }
    return null;
  }

}

可見其具體從 ResultSet 取出 JDBC 類型爲 Timestamp,然後轉換成 java 類型的 Date

實現具體的子類,那麼在哪裏使用了那?其實 mybatis 框架會把所有 TypeHandler 在 TypeHandlerRegistry 註冊。具體類方法如圖

TypeHandlerRegistry

其提供了相關 register 方法註冊 TypeHandler,然後又提供了相關 getTypeHandler 方法取出具體 TypeHandler 實現類。

總結

使用模板方法,將公共邏輯抽取出來,將具體實現細節交給子類。

參考

  1. Mybatis源代碼分析之類型轉換
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章