策略模式的學(xué)習(xí)

1. 問(wèn)題來(lái)源

在年初看某業(yè)務(wù)線清結(jié)算部分代碼時(shí),遇到這么一串代碼,大致如下:


//如果商戶文件到達(dá),使用ftp下載文件
if (map.get(SystemConstant.SYSTEM_TYPE).equals("2")) {
    try{
            ... ...

        ApacheFTPUtil plugins=new ApacheFTPUtil();
        plugins.execute(paramMap);//下載文件路徑信息在paramMap中

        businessLogger1.info(... + "FTP文件下載結(jié)束");
    }catch(Exception e){
        ... ...
    }
}
//如果文件下載到文件核心完成
if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
    businessLogger1.info(... + "文件到達(dá)檢測(cè)啟動(dòng)");

    BaseComponentPlugins plugins = new FileArrive();//調(diào)用文件級(jí)實(shí)現(xiàn)
    result = new TaskResult();
    plugins.execute(map, result);

    businessLogger1.info(... + "文件到達(dá)檢測(cè)結(jié)束");
    
    //文件級(jí)檢測(cè)完成且沒(méi)有問(wèn)題,開(kāi)始進(jìn)行記錄級(jí)檢查
    if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
        businessLogger1.info(... + "文件校驗(yàn)啟動(dòng)");
        result = new TaskResult();

        plugins = new FileValid();
        plugins.execute(map, result);//調(diào)用記錄級(jí)實(shí)現(xiàn)

        businessLogger1.info(... + "文件校驗(yàn)結(jié)束");

        //文件記錄入庫(kù)
        if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
            businessLogger1.info(... + "異步入庫(kù)啟動(dòng)");
            result = new TaskResult();

            plugins = new FileEnterDB();
            plugins.execute(map, result);//調(diào)用異步入庫(kù)實(shí)現(xiàn)

            businessLogger1.info(... + "異步入庫(kù)結(jié)束");

            //入庫(kù)HDFS
            if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
                businessLogger1.info(map, "入庫(kù)HDFS開(kāi)始");
                result = new TaskResult();
                
                plugins = new HdfsLoad();
                plugins.execute(map, result);//調(diào)用入庫(kù)HDFS實(shí)現(xiàn)

                businessLogger1.info(map, "入庫(kù)HDFS結(jié)束");
            }else{
                ... ...//入庫(kù)HDFS異常
            }           
        }else{
            ... ...//文件記錄入庫(kù)異常
        }
    }else{
        ... ...//文件記錄級(jí)入庫(kù)/異步校驗(yàn)異常
    }
}else{
    ... ...//文件級(jí)校驗(yàn)異常
}

額外說(shuō)明的是:

  • 由于清算步驟前后因果關(guān)系,必須前一步執(zhí)行成功才能繼續(xù)下一步,所以代碼結(jié)構(gòu)是這樣多個(gè)if嵌套的樣子
  • 哪一步失敗就需要立即停止并反饋到日志或者數(shù)據(jù)庫(kù),各個(gè)else的代碼也就是處理這些異常的,此處也沒(méi)有列出。

代碼中多次使用到了這樣的結(jié)構(gòu):

BaseComponentPlugins plugins = new ... ...
plugins.execute(map, result);

可能熟悉策略模式的人早就明白這是怎么回事了,但是我剛開(kāi)始看得時(shí)候就只是覺(jué)得,簡(jiǎn)潔明了、維護(hù)和可讀性都很高,再就是感嘆接口原來(lái)是這么用的。

2. 策略模式是什么?

最近在看資料的時(shí)候,偶然看到幾篇介紹策略模式的文章,才慢慢理解了?;仡櫟竭@段代碼的時(shí)候,也才漸漸明白這就是策略模式的實(shí)現(xiàn)。
策略模式的理解可以參見(jiàn)這篇文章,例子生動(dòng)形象:【行為型模式十五】策略模式(Strategy)
我的理解就是把一個(gè)個(gè)具體的算法和使用算法的類是進(jìn)行解耦,比如結(jié)算業(yè)務(wù)的例子被拆分成:

  • FTP下載文件
  • 文件級(jí)校驗(yàn)
  • 記錄及校驗(yàn)
  • 異步入庫(kù)
  • 入庫(kù)HDFS

每種操作都單獨(dú)形成一個(gè)單獨(dú)的算法類,它們共同實(shí)現(xiàn)同一個(gè)接口、接收相同參數(shù)結(jié)構(gòu)的參數(shù),而具體使用哪一個(gè)由使用算法的類來(lái)選擇。
接口定義如下:


package com.xxxx.commonbase.plugins;

import java.util.Map;

import com.xxxx.platform.bean.TaskResult;

public interface BaseComponentPlugins {
    abstract int execute(Map<String, String> map, TaskResult result);
}

再通過(guò)類圖關(guān)系可以更清楚的看到算法實(shí)現(xiàn)與策略接口的關(guān)系:

預(yù)處理

上圖的關(guān)系可以看到,這種處理方式很接近策略模式了,但是PretreatmentDeal類中并沒(méi)有持有策略(也就是BaseComponentPlugins接口)的引用,為了完成結(jié)算它還是通過(guò)先調(diào)用FileArrive、FileValid... ... 這樣一步步完成的,只是實(shí)例化這些對(duì)象使用BaseComponentPlugins引用而已,如之前所述:

BaseComponentPlugins plugins = new ... ...
plugins.execute(map, result);

而真正的策略模式,除了策略接口規(guī)范一系列具體的策略算法所應(yīng)該完成的事;還需要上下文context類要持有策略接口,當(dāng)調(diào)用這個(gè)上下文類的對(duì)象時(shí),會(huì)決定使用哪種策略實(shí)現(xiàn)。
如果真的要以策略模式的實(shí)現(xiàn),這個(gè)PretreatmentDeal類就必須先有一個(gè)策略的成員變量,修改后的類關(guān)系圖應(yīng)該如下:


使用策略模式的預(yù)處理

當(dāng)然原本PretreatmentDeal類的各種if條件需要抽取形成新的調(diào)用類,由這個(gè)調(diào)用類來(lái)判斷每一步的結(jié)果并決定是否更換策略,PretreatmentDealContext只是簡(jiǎn)單調(diào)用excetu()方法并返回結(jié)果。

3.業(yè)務(wù)線強(qiáng)相關(guān)的數(shù)據(jù)源切換

最近集中管控臺(tái)需要管理使用多個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)的選擇只與業(yè)務(wù)線相關(guān)聯(lián).在最開(kāi)始的時(shí)候我能想到的代碼可能也就是類似下面的if判斷形式了.


//依據(jù)產(chǎn)品和業(yè)務(wù)線取不同的數(shù)據(jù)庫(kù)資源
if (paramMap.getProduct.equals(SystemConstant.PRODUCT_A)) {
    if (paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_A)) {
        ... ...//取A業(yè)務(wù)線對(duì)應(yīng)的數(shù)據(jù)庫(kù)
    }else if(paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_B)) {
        ... ...//取B業(yè)務(wù)線對(duì)應(yīng)的數(shù)據(jù)庫(kù)
    }else if(paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_C)) {
        ... ... //取C業(yè)務(wù)線對(duì)應(yīng)的數(shù)據(jù)庫(kù)
    }
}else if(paramMap.getProduct.equals(SystemConstant.PRODUCT_B) {
    if (paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_A)) {
        ... ...//取A業(yè)務(wù)線對(duì)應(yīng)的數(shù)據(jù)庫(kù)
    }else if(paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_B)) {
        ... ...//取B業(yè)務(wù)線對(duì)應(yīng)的數(shù)據(jù)庫(kù)
    }   
}

這樣的寫法缺點(diǎn)在策略模式的講解文章已經(jīng)談了無(wú)數(shù)遍了,所以這個(gè)可以考慮修改下代碼,抽象出數(shù)據(jù)源的獲取方式/算法,新增一個(gè)contex類拿到獲取數(shù)據(jù)源的接口引用。
這樣,當(dāng)service層代碼獲取數(shù)據(jù)源的時(shí)候,只需要依據(jù)業(yè)務(wù)現(xiàn)選擇哪種策略/數(shù)據(jù)源就可以了,大致的思路如下:

使用策略模式的多數(shù)據(jù)源獲取

剩下的工作就是service層依據(jù)controller層的參數(shù),做出選擇:

  • 依據(jù)產(chǎn)品product選擇使用哪種策略(即哪種獲取數(shù)據(jù)庫(kù)資源的方式,比如oracle、mysql),
  • 依據(jù)具體業(yè)務(wù)現(xiàn)選擇哪個(gè)數(shù)據(jù)源(比如同一個(gè)數(shù)ip下不同數(shù)據(jù)庫(kù)用戶名管理的表等等)
    這樣,可以避免大量的判斷條件。上下文類DataSourceContext代碼寫法較為固定,類似這樣:

/**
* 數(shù)據(jù)源管理,完成向調(diào)用者返回匹配的數(shù)據(jù)庫(kù)資源
*/
public class DataSourceContext {
  /**
   * 持有一個(gè)具體的策略對(duì)象(數(shù)據(jù)庫(kù)資源接口)
   */
  private AccessDataSource AccessDataSource = null;
  
  /**
   * 構(gòu)造方法,傳入一個(gè)具體業(yè)務(wù)線數(shù)據(jù)庫(kù)資源對(duì)象
   */
  public DataSourceContext(Strategy aStrategy){
      this.strategy = aStrategy;
  }  
  
  /**
   * 獲取數(shù)據(jù)庫(kù)資源
   * @param bussinessChannel業(yè)務(wù)線編碼
   * @return 業(yè)務(wù)線關(guān)聯(lián)的數(shù)據(jù)庫(kù)資源
   */
  public double getDataSource(String bussinessChannel){
      return this.strategy.getDataSource(bussinessChannel);
  }
}

這么做就具備了很強(qiáng)的靈活性,比如配置不同類型數(shù)據(jù)庫(kù)、同一個(gè)數(shù)據(jù)庫(kù)多個(gè)數(shù)據(jù)庫(kù)用戶的管理;尤其是新增一個(gè)業(yè)務(wù)現(xiàn),只需新增業(yè)務(wù)線

4.一點(diǎn)感受

凡事有利有弊,策略模式要求調(diào)用者必須清楚地知道每種策略實(shí)現(xiàn),這樣才能在選擇的時(shí)候傳遞給context類具體的策略。比如這個(gè)數(shù)據(jù)源的例子調(diào)用類就必須的寫成:


public class ProductAUserServiceImpl {
    
    //選擇并創(chuàng)建需要使用的數(shù)據(jù)源,需要顯示聲明使用哪一種
    AccessDataSource strategy = new ProductADataSource();
    
    //創(chuàng)建上下文對(duì)象
    DataSourceContext context =  new DataSourceContext(strategy);
    
    /**
    * 依據(jù)業(yè)務(wù)線bussinessChannel拿到具體mybatis dao
    */
    public UserMapperDao UserMapperDao(String bussinessChannel){
        return context.strategy.getDataSource(bussinessChannel);
    }
    
    /**
    * 具體查詢實(shí)現(xiàn),供給controller層使用
    */
    public List<User> queryByName(String bussinessID, String name) {
        this.UserMapperDao(bussinessID).queryByName(name);
    }
    
}

也有說(shuō)法可以調(diào)用者不需要知道具體哪些策略,完全有context類依據(jù)參數(shù)控制,沒(méi)有在繼續(xù)看,但感覺(jué)如果處理不好、context類又要出現(xiàn)一堆if判斷,到時(shí)又得想辦法處理掉這些判斷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容