快速迭代業務背景下:設計模式混編的一次嘗試

程式猿的十萬個為什麼發表於2020-11-13

​一、業務背景

在現有集團業務中,快速迭代已經是家常便飯了,立項需求,評估排期,進入開發,提前進入測試。作為專案任何一個環節的小夥伴應該都有體會,快速反應快速迭代已經成為常態。如果假如你參與的是一個穩定服務的中臺專案,那麼還好一些能夠持續的思考優化專案。如果只是一種活動性質的專案,這次做完下次業務就完全不一樣了加上時間短任務重,導致沒有那麼多時間去做設計,設計模式的作用在這種型別的專案中作用就被削弱了。設計模式的使用比較常見是中介軟體、技術框架中。因為設計模式的主要能力是解耦,所以在中介軟體和技術框架中便發揮了很大的優勢。在接入集團安全風控的介面工作中,做了一些思考,接入安全風控有兩種方式,一種是通過http閘道器方式,另外一種是通過rpc閘道器的方式,rpc方式更加靈活,http更加簡潔,配置成本更低。由於業務發展,現在需要根據不同的渠道做不同的預算,因此需要配置不同的安全策略,每一種策略對應一種安全碼作為接入標識,同時考慮到後期針對不同的業務會定製自己的安全策略。因此做了一個設計模式混編的設計,目的是為了“一勞永逸”。如果對這種“一勞永逸”感興趣,請繼續閱讀下去,相信你會有收穫。

 

二、設計思路

結合業務背景,考慮到將來會擴充更多的渠道,每次接入都需要進行擴充套件,如果在接入中使用if else會影響以前的程式碼,測試需要回歸,只會增加測試工作量。考慮到以上原因,現在使用門面模式+責任鏈+策略模式,本著OCP原則,對修改關閉,對擴充套件開放。

思路類圖:

三、模式優缺點總結

1、門面模式

門面模式定義:門面模式(Facade Pattern) 也叫做外觀模式,要求一個子系統的外部與其內部的通訊必須通過一個統一的物件進行。門面模式提供一個高層次的介面, 使得子系統更易於使用。 

類圖如下:

1.1門面呼叫示例程式碼

public class AlisecurityFacade implements IalisecurityFacade {
    @Autowired
    private StrategeContext context;

    /**
     * 門面屬於高層模組,一旦設計好和外部系統的互動規則,一般不再做改動,
     * 相應的業務變動在底層模組中,此處底層模組是責任鏈和策略
     * @param channelCode
     * @param token
     * @return
     */
    @Override
    public  Boolean verify(int channelCode,String token) {
        CheckUtil.isEmpty(channelCode, "渠道號");
        CheckUtil.isEmpty(token, "token");
        //通過門面訪問具體的操作,遵守【迪米特法則】
        Boolean result = context.fire(channelCode,token);
        return result;
    }

}

 

1.2門面封裝示例

通過統一的入口呼叫,只需要提供一個方法入口,呼叫者無需關注方法中的具體邏輯流程,就算方法裡“敗絮其中”,方法外也可以“金玉其外”,遵循迪米特法則,最少知道原則。

public class StrategeContext {
    @Autowired
    private StrategyMofun strategyMofun;

    @Autowired
    private StrategyTmall strategyTmal;

    /**
     * 通過封裝,對鏈路比較長的邏輯進行封裝,供門面使用,每一條邏輯儘量遵循【單一職責原則】
     * @param channelCode
     * @param token
     * @return
     */
    public Boolean fire(int channelCode,String token) {
        //①流程一:設定責任鏈順序
        strategyTmal.setNextHandler(strategyMofun);
        //②流程二:通過責任鏈選擇合適的策略
        Istratege excuteStratege = strategyTmal.handleMessage(channelCode);
        //③流程三:呼叫策略中的安全校驗方法
        Boolean result = excuteStratege.verifySecurity(token);
        return result;
    }

}

 

1.3門面優點

1.3.1減少系統的相互依賴


想想看, 如果我們不使用門面模式, 外界訪問直接深入到子系統內部, 相互之間是一種強耦合關係, 你死我就死, 你活我才能活, 這樣的強依賴是系統設計所不能接受的, 門面模式的出現就很好地解決了該問題, 所有的依賴都是對門面物件的依賴, 與子系統無關。

1.3.2提高了靈活性


依賴減少了, 靈活性自然提高了。不管子系統內部如何變化, 只要不影響到門面物件,任你自由活動。

 

1.3.3 提高安全性


想讓你訪問子系統的哪些業務就開通哪些邏輯, 不在門面上開通的方法, 你休想訪問到

 

1.4門面缺點

 

門面是最頂層的邏輯,如果門面實在無法滿足互動需求,門面改動,底層會有蝴蝶效應,改動會很大,門面設計要慎重。

1.5

門面模式的使用場景

1.5.1

為一個複雜的模組或子系統提供一個供外界訪問的介面

1.5.2

子系統相對獨立——外界對子系統的訪問只要黑箱操作即可

 

2、責任鏈模式

定義:使多個物件都有機會處理請求, 從而避免了請求的傳送者和接受者之間的耦合關係。將這些物件連成一條鏈, 並沿著這條鏈傳遞該請求, 直到有物件處理它為止 , 責任鏈模式的重點是在“鏈”上, 由一條鏈去處理相似的請求在鏈中決定誰來處理這個請求, 並返回相應的結果 。
 

類圖如下:

2.1示例程式碼

@Slf4j
@Component
public class StrategyMofun extends Handler implements Istratege {

    @Autowired
    private  StrategyMofun strategyMofun;

    /**
     * 安全碼
     */
    @Value("${alibaba.security.mofun.asac}")
    private String asac;

    @Autowired
    private RmbService rmbService;

    public StrategyMofun() {
        super(Handler.MOFUN_CHANNEL_CODE);
    }

    @Override
    public Boolean verifySecurity(String token) {
        log.info("小程式策略校驗邏輯");
        // 在此處寫具體的安全校驗邏輯策略
        if (!SwitchProperties.mofunRmbStatus.get()) {
            return true;
        }
        return rmbService.getRmbStatus(token, asac);
    }

    @Override
    public Istratege getStrategeInstance() {
//        Class<? extends StrategyMofun> aClass = this.getClass();
//        String className = aClass.getName();
        return strategyMofun;
    }
}
@Slf4j
public abstract class Handler {
    //天貓、淘寶的渠道code
    public final static int TMALL_CHANNEL_CODE = 2;
    //魔放的渠道code
    public final static int MOFUN_CHANNEL_CODE = 1;
    private int channelCode = 0;

    /**
     * 責任傳遞,下一個責任處理者是誰
     */
    private Handler nextHandler;
    /**
     * 獲得責任鏈上責任者的例項(策略)
     */
    private Object stratege;

    public Handler(int channelCode) {
        this.channelCode = channelCode;
    }

    public Istratege handleMessage(int channelCode) {
        Istratege strategeInstance = getStrategeInstance();
        if (channelCode == this.channelCode) {
//            Istratege stratege = null;
//            try {
//                //拿到責任鏈中節點的例項
//                stratege = (Istratege) Class.forName(strategeInstance).newInstance();
//            } catch (Exception e) {
//                log.error("使用反射生成策略例項異常{}", e);
//            }
            return strategeInstance;
        } else {
            //有後續責任者,把請求接著往下傳
            if (this.nextHandler != null) {
                return this.nextHandler.handleMessage(channelCode);
            } else {
                //沒有後繼處理的責任人,應放過,不做攔擊校驗
                return null;
            }
        }
    }

    /**
     * 如果不屬於當前責任者,應傳遞下一個責任者
     *
     * @param handler
     */
    public void setNextHandler(Handler handler) {
        this.nextHandler = handler;
    }

    protected abstract Istratege getStrategeInstance();
}

 

2.1優點

責任鏈模式非常顯著的優點是將請求和處理分開。請求者可以不用知道是誰處理的, 處理者可以不用知道請求的全貌(例如在J2EE專案開發中, 可以剝離出無狀態Bean由責任鏈處理) , 兩者解耦, 提高系統的靈活性

 

2.2缺點

責任鏈有兩個非常顯著的缺點:一是效能問題, 每個請求都是從鏈頭遍歷到鏈尾, 特別是在鏈比較長的時候, 效能是一個非常大的問題。二是除錯不很方便, 特別是鏈條比較長,環節比較多的時候, 由於採用了類似遞迴的方式, 除錯的時候邏輯可能比較複雜

 

2.3 責任鏈模式的注意事項

鏈中節點數量需要控制, 避免出現超長鏈的情況, 一般的做法是在Handler中設定一個最大節點數量, 在setNext方法中判斷是否已經是超過其閾值, 超過則不允許該鏈建立, 避免無意識地破壞系統效能。

 

3、策略模式

定義:定義一組演算法, 將每個演算法都封裝起來, 並且使它們之間可以互換

類圖如下:

3.1示例程式碼

public interface Istratege {
    /**
     * 策略的抽象介面
     * @param token
     * @return
     */
     Boolean verifySecurity(String token);
}

@Slf4j
@Component
public class StrategyMofun extends Handler implements Istratege {

    @Autowired
    private  StrategyMofun strategyMofun;

    /**
     * 安全碼
     */
    @Value("${alibaba.security.mofun.asac}")
    private String asac;

    @Autowired
    private RmbService rmbService;

    public StrategyMofun() {
        super(Handler.MOFUN_CHANNEL_CODE);
    }

    @Override
    public Boolean verifySecurity(String token) {
        log.info("小程式策略校驗邏輯");
        // 在此處寫具體的安全校驗邏輯策略
        if (!SwitchProperties.mofunRmbStatus.get()) {
            return true;
        }
        return rmbService.getRmbStatus(token, asac);
    }

    @Override
    public Istratege getStrategeInstance() {
//        Class<? extends StrategyMofun> aClass = this.getClass();
//        String className = aClass.getName();
        return strategyMofun;
    }
}

3.2優點

3.2.1演算法可以自由切換


這是策略模式本身定義的, 只要實現抽象策略, 它就成為策略家族的一個成員, 通過封裝角色對其進行封裝, 保證對外提供“可自由切換”的策略。

 

3.2.2 避免使用多重條件判斷


如果沒有策略模式, 我們想想看會是什麼樣子?一個策略家族有5個策略演算法, 一會要使用A策略, 一會要使用B策略, 怎麼設計呢?使用多重的條件語句?多重條件語句不易維護, 而且出錯的概率大大增強。使用策略模式後, 可以由其他模組決定採用何種策略, 策略家族對外提供的訪問介面就是封裝類, 簡化了操作, 同時避免了條件語句判斷。

 

3.2.3擴充套件性良好


這甚至都不用說是它的優點, 因為它太明顯了。在現有的系統中增加一個策略太容易了, 只要實現介面就可以了, 其他都不用修改, 類似於一個可反覆拆卸的外掛, 這大大地符合了OCP原則

 

3.3缺點

策略類數量增多
每一個策略都是一個類, 複用的可能性很小, 類數量增多。

 

3.4 策略模式的使用場景

3.4.1多個類只有在演算法或行為上稍有不同的場景。

3.4.2演算法需要自由切換的場景。


例如, 演算法的選擇是由使用者決定的, 或者演算法始終在進化, 特別是一些站在技術前沿的行業, 連業務專家都無法給你保證這樣的系統規則能夠存在多長時間, 在這種情況下策略模式是你最好的助手。

 

3.4.3需要遮蔽演算法規則的場景。


現在的科技發展得很快, 人腦的記憶是有限的(就目前來說是有限的) , 太多的演算法你只要知道一個名字就可以了, 傳遞相關的數字進來, 反饋一個運算結果, 萬事大吉。

 

3.5 策略模式的注意事項

 

如果系統中的一個策略家族的具體策略數量超過4個, 則需要考慮使用混合模式, 解決策略類膨脹和對外暴露的問題。

四、除錯筆記

使用中,在選擇鏈中節點例項返回時,使用了反射生成例項,生成的例項會把依賴的注入例項過濾掉,因為是組合關係,反射目標類時,組合類不作為目標類的後設資料來進行載入。這個地方也犯了一個很大的思考錯誤,專案本來已經使用spring依賴注入了,自己還多此一舉,就是在騎驢找驢。

反射的效能問題:

導致反射慢的兩個原因:

①,反射是基於程式集和後設資料的,在使用反射的時候,會搜尋後設資料,而後設資料是基於字串的,並且無法預編譯,所以這一系列的操作對效能有影響。

②,大量的裝箱拆箱也對效能有影響。由於我們對目標型別是未知的,而且向方法傳遞的引數通常是object型別的,所以會有大量的裝箱和拆箱。

反射使用注意:

反射大概比直接呼叫慢50~100倍,但是需要在執行100萬遍的時候才會有所感覺判斷一個函式的效能,需要把這個函式執行100萬遍甚至1000萬遍如果只是偶爾呼叫一下反射,可以不必關心反射帶來的效能影響如果需要大量呼叫反射,需要考慮快取。程式設計的思想才是限制程式效能的最主要的因素

五、總結

看完以後,是否感覺可以“一勞永逸”,只要是在執行的業務就不可能會真正的一勞永逸,但是設計模式可以讓程式足夠鬆耦合甚至可插拔,下次迭代開發時能夠達到減少複雜度,相對一勞永逸的效果。

交流

馬老師曾經說過,“未來資料是共同分享的,才能提現它的價值!“,如果有更多專案使用場景,歡迎留言交流


如果您已閱讀到此,感謝您對公眾號裡文章的認可,請動動你可愛的小指頭關注微信公眾號,每天都有硬核技術文章推送,就算離開也能找到回家的路

二維碼如下或者公眾號搜尋“程式設計師的十萬個為什麼”:

相關文章