原始碼中的設計模式--模板方法模式

北漂程式設計師發表於2022-05-14

本文要解決的幾個問題,

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/

相關文章