本文要解決的幾個問題,
1、什麼是模板方法模式;
2、模板方法模式的使用場景;
3、模板方法模式的優點;
4、原始碼中有哪些地方使用到了模板方法模式;
帶著這幾個問題,我們開始今天的設計模式的分享。
一、模式入場
大家在日常的工作生活中肯定碰到過這樣的場景,比如,你要轉正答辯了,總要有個PPT吧,這時你是不是會問你同事要個述職的PPT模板,有個模板的好處這裡自不用說。你去幫助單位去投標拿專案了,你是不是要問甲方爸爸要個模板,按照模板準備你的材料。生活中這樣的例子太多了,有模板好辦事。
在平時的開發過程中,不知道你是否碰到過類似的情形,你要呼叫系統A和系統B的介面,把系統A和系統B的資料讀取過來,經過處理儲存到自己的資料庫裡。
針對這樣的場景你要怎麼設計吶,首先,針對這樣一個場景進行分析,要明確的是需要呼叫兩個系統的介面,這兩個系統返回的資料是不一樣的,並且要儲存到不同的表中,下面先試圖實現下這個場景。有兩個類SyncSystemA和SyncSystemB分別表示處理系統A和系統B的介面資料,
SyncSystemA.java
package com.example.template;
public class SyncSystemA {
public void syncData(){
//1、組裝引數
String url="http://a.com/query";
String param="A";
//2、傳送請求
String result=sendRequest(url,param);
//3、解析
String result2=parse(result);
//4、儲存資料
saveData(result2);
}
private String sendRequest(String url,String param){
System.out.println("傳送請求到A");
return "";
}
private String parse(String result){
System.out.println("對A返回結果進行解析");
return "";
}
private void saveData(String result){
System.out.println("儲存A的資料");
}
}
SyncSystemB.java
package com.example.template;
public class SyncSystemB {
public void syncData(){
//1、組裝引數
String url="http://b.com/query";
String param="A";
//2、傳送請求
String result=sendRequest(url,param);
//3、解析
String result2=parse(result);
//4、儲存資料
saveData(result2);
}
private String sendRequest(String url,String param){
System.out.println("傳送請求到B");
return "";
}
private String parse(String result){
System.out.println("對B的返回結果進行解析");
return "";
}
private void saveData(String result){
System.out.println("儲存B的資料");
}
}
下面看測試方法,Test.java
package com.example.template;
public class Test {
public static void main(String[] args) {
SyncSystemA syncSystemA=new SyncSystemA();
SyncSystemB syncSystemB=new SyncSystemB();
syncSystemA.syncData();
System.out.println("-----------");
syncSystemB.syncData();
}
}
返回結果如下,
傳送請求到A
對A返回結果進行解析
儲存A的資料
-----------
傳送請求到B
對B的返回結果進行解析
儲存B的資料
Process finished with exit code 0
可以看到很好的完成了我們的目標,那就是同步系統A和系統B的資料。但是從上面的程式碼中也能發現一些問題,在SyncSystemA和SyncSystemB中有很多的重複程式碼,追求極簡的我們怎麼能容忍這樣的程式碼。
二、深入模板方法模式
上面的處理步驟其實可以歸納為下面的流程,如下圖,
我們把這樣一個過程抽象出了這樣幾步:組裝引數、傳送請求、解析引數、儲存資料,在這樣幾步中組裝引數和解析引數肯定是不同的,對於傳送請求和儲存資料我們可以把它們處理成一致的。既然有一樣的處理步驟,為了減少重複的程式碼,我們可以進行優化,把公共的部分抽取出來,那麼如何才能實現這樣的目的,可以把公共的部分封裝到工具類中,在不同的地方進行呼叫,但這些方法又不能算的上是工具類。還有一個方法在java基礎中有抽象類的概念,今天就使用下抽象類,那麼如何設計抽象類,下面看,
AbstractSyncData.java
package com.example.template;
import java.util.Map;
public abstract class AbstractSyncData {
//定義好同步資料的步驟
public void syncData() {
//1、組裝引數
Map param = assembleParam();
//2、傳送請求
String result = sendRequest(param);
//3、解析
String result2 = parse(result);
//4、儲存資料
saveData(result2);
}
//1、組裝引數,供子類實現自己的邏輯
protected abstract Map assembleParam();
//2、傳送請求
private String sendRequest(Map map) {
//實際傳送請求,並把資料返回
System.out.println("傳送請求");
return "";
}
//3、解析返回結果,供子類實現自己的邏輯
protected abstract String parse(String result);
//4、儲存資料
private void saveData(String result) {
System.out.println("儲存資料");
}
}
從上面的AbstractSyncData抽象類中,可以看到把syncData放到了抽象類中,並且在該類中定義了完成此功能的步驟:組裝引數、傳送請求、解析返回結果、儲存資料,其中組裝引數、解析返回結果兩步在抽象類中定義了抽象方法,定義抽象方法的目的是為了讓自己去實現自己的邏輯,看下兩個子類的實現,
SyncSystemAImpl.java
package com.example.template;
import java.util.HashMap;
import java.util.Map;
public class SyncSystemAImpl extends AbstractSyncData{
@Override
protected Map assembleParam() {
System.out.println("組裝傳送到系統A的引數");
return new HashMap();
}
@Override
protected String parse(String result) {
System.out.println("解析系統A的返回結果");
return "";
}
}
SyncSystemBImpl.java
package com.example.template;
import java.util.HashMap;
import java.util.Map;
public class SyncSystemBImpl extends AbstractSyncData{
@Override
protected Map assembleParam() {
System.out.println("組裝傳送到系統B的引數");
return new HashMap();
}
@Override
protected String parse(String result) {
System.out.println("解析系統B的返回結果");
return "";
}
}
看下測試結果
組裝傳送到系統A的引數
傳送請求
解析系統A的返回結果
儲存資料
-----------
組裝傳送到系統B的引數
傳送請求
解析系統B的返回結果
儲存資料
Process finished with exit code 0
看到上面的結果同樣實現了功能,而且從程式碼風格上是不是更簡潔,而且使用到了模板方法模式。
看下《Head First 設計模式》一書中給模板方法模式下的定義
模板方法模式在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。
上面的釋義定義的太完美了,多讀幾遍上面的釋義和我們上面的AbstractSyncData類對比下
演算法的骨架對應syncData方法
一些步驟延遲到子類對應assembleParam和parse方法
重新定義演算法中的某些步驟對應assembleParam和parse方法,因為針對不同的實現有不同的處理邏輯。
模板方法的使用場景上面已經提到過,在開發中要善於抽象,把一個場景中的步驟抽象成不同的幾步,如果有多種實現,那麼此時便是使用模板方法的大好時機。
番外
想多說一句的是,現在不是都談面向介面程式設計,那麼針對面向介面程式設計我們要如何改造上面的模板方法模式吶,只需要把syncData放到介面中即可,
package com.example.template;
public interface SyncData {
//同步資料
void syncData();
}
相應的抽象類實現該介面即可,
其UML圖如下,
三、追尋原始碼
上面已經系統的學習了模板方法模式,下面看下在原始碼中的使用,
1、mybatis的BaseExecutor
在mybatis的BaseExecutor類中有update方法,
該方法來自於介面Executor,該方法又呼叫了doUpdate方法,該方法在BaseExecutor中是抽象方法,
看下實現的子類,
和我們上面的例子是不是很像,或者說就是同一個,再看下其uml
2、spring的AbstractApplicationContext
在spring的AbstractApplicaitonContext類中有fresh()方法,該方法中呼叫了obtainFreshBeanFactory方法,
obtainFreshBeanFactory方法,
看下這兩個方法,
這兩個方法是抽象的,肯定也是模板方法了。
四、總結
模板方法模式的精髓在於抽象,抽象出完成某個功能的步驟,再把個性化的步驟做為抽象方法,讓子類延遲實現,公有的方法在抽象類中完成。在使用模板方法時由於存在抽象類,會出現多個繼承子類的情況,需要視情況而定。另外,模板方法模式可以結合介面使用,實現面向介面程式設計。
首發於:https://www.toutiao.com/article/7097584508639183367/