什么是模板方法模式?摘錄 wiki 的介紹。
模板方法模式定義了一個(gè)算法的步驟,并允許子類(lèi)別為一個(gè)或多個(gè)步驟提供其實(shí)踐方式。讓子類(lèi)別在不改變算法架構(gòu)的情況下,重新定義算法中的某些步驟。在軟件工程中,它是一種軟件設(shè)計(jì)模式,和C++模板沒(méi)有關(guān)連。
模板設(shè)計(jì)方法存在目的在于某些算法邏輯存在一些相同處,而具體細(xì)節(jié)卻不同。這樣使用模板方法,可以抽取共用邏輯到父類(lèi),在子類(lèi)實(shí)現(xiàn)具體算法細(xì)節(jié),這樣減少了重復(fù)代碼。 模板方法充分運(yùn)用了多態(tài)與繼承。使用抽象父類(lèi)定義抽象操作,然后在公共邏輯調(diào)用抽象方法。子類(lèi)方法只要繼承父類(lèi)關(guān)注自身實(shí)現(xiàn)細(xì)節(jié)。
Talk is cheap. Show me the code
下面拿支付接入支付渠道例子來(lái)使用模板方法。
假設(shè)銀行卡支付需要實(shí)現(xiàn)兩家銀行的支付功能。不同銀行提供的接口,在參數(shù),調(diào)用方式等肯定存在很大區(qū)別。這個(gè)時(shí)候我們就可以使用模板設(shè)計(jì)方法,父類(lèi)實(shí)現(xiàn)支付前通用邏輯,用子類(lèi)實(shí)現(xiàn)交互的不同。系統(tǒng)類(lèi)結(jié)構(gòu)如下。

AgreementPay 提供支付功能,AgreementBasePay 為抽象類(lèi)實(shí)現(xiàn)通用邏輯,AgreementCCBPay 與 AgreementCMBPay 實(shí)現(xiàn)具體的渠道支付方法。具體源碼如下。
AgreementPay 接口
public interface AgreementPay {
PayResponse payInChannel(PayRequest request);
}
AgreementBasePay 抽象方法實(shí)現(xiàn)通用邏輯。
public abstract class AgreementBasePay implements AgreementPay {
public PayResponse pay(PayRequest request) {
checkRequest(request);
return this.payInChannel(request);
}
private void checkRequest(PayRequest request) {
System.out.println("具體方法參數(shù)檢查");
}
}
具體實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)具體渠道支付細(xì)節(jié)。
public class AgreementCCBPay extends AgreementBasePay {
@Override
public PayResponse payInChannel(PayRequest request) {
System.out.println("去建設(shè)銀行支付");
return new PayResponse();
}
}
public class AgreementCMBPay extends AgreementBasePay {
@Override
public PayResponse payInChannel(PayRequest request) {
System.out.println("去招商銀行支付");
return new PayResponse();
}
}
實(shí)現(xiàn)模板方法的細(xì)節(jié),我們來(lái)看 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("使用建設(shè)銀行支付");
agreementPay = new AgreementCCBPay();
agreementPay.payInChannel(request);
}
}
上面 client 邏輯,其實(shí)看起來(lái)還是有一些死板,且需要外部知道調(diào)用哪個(gè)渠道接口。但是如果真正提供一個(gè)對(duì)外接口,外部調(diào)用方法是不關(guān)心你具體使用那個(gè)子類(lèi)支付。所以這里我們可以改進(jìn)一下,
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);
return;
case "CCB":
pa = payCache.get("CCB");
pa.payInChannel(request);
return;
default:
throw new RuntimeException();
}
}
改造之后我們先將其 AgreementPay 實(shí)例放入 map 中,然后調(diào)用時(shí)根據(jù)一個(gè)標(biāo)志來(lái)選擇具體實(shí)現(xiàn)類(lèi)。
從上面的細(xì)節(jié)我們可以看到模板方法其實(shí)設(shè)計(jì)思路與實(shí)現(xiàn)細(xì)節(jié)都比較簡(jiǎn)單??赐晡覀兊氖纠a,我們?nèi)タ聪?mybatis 如何使用模板方法。
mybatis 模板方法應(yīng)用
在看源碼之前,我們先看下我們不使用 mybatis 之前,如何查詢數(shù)據(jù)。
Class.forName("com.mysql.jdbc.Driver");
//2.獲得數(shù)據(jù)庫(kù)的連接
Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD);
//3.通過(guò)數(shù)據(jù)庫(kù)的連接操作數(shù)據(jù)庫(kù),實(shí)現(xiàn)增刪改查
PreparedStatement pstmt = conn.prepareStatement("select user_name,age from imooc_goddess where id=?");
pstmt.setInt(1, 21);
ResultSet rs = pstmt.execute();
while (rs.next()) {//如果對(duì)象中有數(shù)據(jù),就會(huì)循環(huán)打印出來(lái)
System.out.println(rs.getString("user_name") + "," + rs.getInt("age"));
}
我們可以看到直接使用 JDBC 查詢,十分麻煩,且需要我們自己將 java 類(lèi)型轉(zhuǎn)換成 jdbc 數(shù)據(jù)類(lèi)型。
ORM 框架重要作用在于把數(shù)據(jù)庫(kù)表與 java,ORM 框架省去我們自己將 java 類(lèi)型轉(zhuǎn)化成 JDBC 類(lèi)型的麻煩。JDBC 存在有那么多類(lèi)型,如何做到轉(zhuǎn)換的那?其實(shí)關(guān)鍵就是應(yīng)用模板設(shè)計(jì)方法。
mybatis 中存在一個(gè)接口 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;
}
從方法上看,這個(gè)接口主要的方法為 PreparedStatement 設(shè)置列參數(shù),或者從 ResultSet 獲取列的值然后轉(zhuǎn)換成相應(yīng)的 java 數(shù)據(jù)類(lèi)型。我們看下這個(gè)接口實(shí)現(xiàn)的類(lèi)圖。

可以看到 BaseTypeHandler 為 TypeHandler 的具體抽象類(lèi),我們具體看下 TypeHandler getResult 在抽象類(lèi)中實(shí)現(xiàn)細(xì)節(jié)。
@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;
可以看到其最后調(diào)用抽象方法 getNullableResult。其由具體的子類(lèi)的實(shí)現(xiàn)。我們具體找一個(gè)子類(lèi) DateTypeHandler 來(lái)查看具體實(shí)現(xiàn)。
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;
}
}
可見(jiàn)其具體從 ResultSet 取出 JDBC 類(lèi)型為 Timestamp,然后轉(zhuǎn)換成 java 類(lèi)型的 Date。
實(shí)現(xiàn)具體的子類(lèi),那么在哪里使用了那?其實(shí) mybatis 框架會(huì)把所有 TypeHandler 在 TypeHandlerRegistry 注冊(cè)。具體類(lèi)方法如圖

其提供了相關(guān) register 方法注冊(cè) TypeHandler,然后又提供了相關(guān) getTypeHandler 方法取出具體 TypeHandler 實(shí)現(xiàn)類(lèi)。
總結(jié)
使用模板方法,將公共邏輯抽取出來(lái),將具體實(shí)現(xiàn)細(xì)節(jié)交給子類(lèi)。
參考
- Mybatis源代碼分析之類(lèi)型轉(zhuǎn)換
|