本文要解決的幾個問題,
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/