天天看點

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

作者:閑魚技術-尋弈

1 問題背景

最近開發了一個需求,該接口需要根據 p1、p2、p3、version 多個入參的不同組合按照其對應的業務政策給出結果資料。由于該接口已經開發了三期了,每次開發新一期的需求時為了相容老的業務邏輯,大家都傾向于不删不改隻新增,是以這塊代碼已經産生了一些「壞味道」,函數入口通過不斷添加「衛語句」判斷 version 的方式跳轉到新一期的業務邏輯方法中,而每一期的業務邏輯也是通過 p1、p2、p3 的 if-else 組合形成不同的分支邏輯。這已經是我簡化後的表述,總之剛開始對于我這個新同學來說,梳理這塊業務代碼着實花了一些功夫。

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

而且,這塊邏輯相當于是一個業務上的通用能力,未來一定還會有五期、六期、N 期的需求進來,入參的取值也會不斷拓展,是以以現有方式膨脹下去隻會壞味道會越來越重。

總結一下,目前場景面臨的問題是:

  1. 如何解決接口更新,在保證相容老版本的情況下輕松開發新版本業務邏輯?
  2. 如何根據入參 p1、p2、p3 等的不同組合進行政策定位?

2 解決思路

在思考解決方案時,很容易想到兩種可以優化類似場景的設計模式:責任鍊模式和政策模式。

2.1 責任鍊模式

責任鍊模式是實作了類似「流水線」結構的逐級處理,通常是一條鍊式結構,将「抽象處理者」的不同實作串聯起來:如果目前節點能夠處理任務則直接處理掉,如果無法處理則委托給責任鍊的下一個節點,如此往複直到有節點可以處理這個任務。

我們可以通過責任鍊模式完成對不同 version 業務邏輯隔離的處理,比如節點 1 處理 version = 1 的請求,節點 2 處理 version = 2 的請求等等。但問題在于我們遇到的場景還需要根據一定政策,路由到不同的下遊節點進行處理。這就是政策模式擅長解決的問題了。

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

2.2 政策模式

政策模式的目的是将算法的使用與定義解耦,能夠實作根據規則路由到不同政策類進行處理。

我們可以通過政策模式解決根據不同參數組合執行不同業務邏輯的場景。但是我們的場景僅僅通過一層政策路由無法滿足任務處理需求。請求的分層處理又是責任鍊模式所擅長的了。

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

可以看到,兩種設計模式都不完全符合目前這個場景:責任鍊模式可以實作逐級委托,但每一級又不能像政策模式那樣路由到不同的處理者上;政策模式通常隻有一層路由,不易實作多個參數的政策組合。

是以我們自然而然地可以想到:是不是可以将兩種模式結合起來?

2.3 廣義責任鍊模式 - 責任樹模式

将責任鍊與政策模式融合,即成為了一種廣義的責任鍊模式,我簡稱為「責任樹模式」。這種模式不僅可以完成任務的逐級委托,也可以在任一級選擇不同的下遊政策進行處理。

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

那麼問題來了,如何通過責任樹模式解決前面我們遇到的問題呢?

首先看如何解決第一個問題,新老接口的隔離 & 相容:可以将接口每個版本的邏輯作為一個責任樹上第一層的不同實作,如分别對應上圖中的 Strategy1、Strategy2、Strategy3 節點。這樣在接口入口,就首先把政策路由到不同的分支上去。如果沒有節點命中,則不再向下遊委托直接傳回錯誤。

然後第二個問題,參數的組合定位到不同的政策實作上:同樣的思路,一個參數對應責任樹上的一層的路由,将該參數的不同取值路由到下一層的不同實作即可,這樣逐級委托,後面新增入參的枚舉值、甚至再拓展新的入參都可以非常友善地進行拓展。

3 優化收益

将這塊業務通過「責任樹模式」重構之後,可以收獲以下幾個收益點:

  1. 後續疊代人力成本降低;
  2. 代碼結構更清晰,可維護性提升:沒有了各種衛語句的跳轉 & 維護性巨差的巨型方法,函數可以收斂在理想的 50 行内;
  3. 後續新增需求修改代碼不易出錯:政策間隔離,不需要完整看一遍大函數理清邏輯再修改,隻需要無腦添加一條路由 + 新的政策實作方法即可;
  4. 問題易定位:同樣由于政策間隔離,調試時可以直接定位到指定政策的業務邏輯代碼,不需要逐句排查;

相信有開發經驗的同學應該都有體會,即使是自己寫過的代碼,一陣子不看也會忘掉,等到再有修改時,還要順着代碼理一遍邏輯,如果文檔、注釋沒寫好,那就更加酸爽了。是以,将巨型函數拆分解耦非常重要。

4 抽象架構

雖然通過「責任樹模式」解決了我這個需求開發中遇到的問題,但是類似的問題還是普遍存在的。本着助(shǎo)人(zào)為(lún)樂(zi)的精神,我更進一步,将責任樹模式抽象出一個通用的架構,友善大家在遇到類似問題時快速「種樹」。

這個架構由一個 Router 和 Handler 組成:

  • Router 是一個抽象類,負責定義如何路由到下遊的多個子節點;
  • Handler 是接口,負責實作每個節點的業務邏輯。

我們可以非常友善地通過 Router 和 Handler 的組合拼裝成整棵樹的結構。

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

從圖中我們可以看出以下幾個要點:

  1. 除了根節點(入口)外,每個節點都實作了 Handler 接口。根節點隻繼承 Router 抽象類;
  2. 所有葉子節點隻實作 Handler 接口而無需繼承 Router 抽象類(無需再向下委托);
  3. 除了根節點和葉子節點外的其他節點,都是上一層的 Handler,同時是下一層的 Router;

那麼我們話不多說,先看下架構代碼。

3.1 AbstractStrategyRouter 抽象類

/**
 * 通用的“政策樹“架構,通過樹形結構實作分發與委托,每層通過指定的參數進行向下分發委托,直到達到最終的執行者。
 * 該架構包含兩個類:{@code StrategyHandler} 和 {@code AbstractStrategyRouter}
 * 其中:通過實作 {@code AbstractStrategyRouter} 抽象類完成對政策的分發,
 * 實作 {@code StrategyHandler} 接口來對政策進行實作。
 * 像是第二層 A、B 這樣的節點,既是 Root 節點的政策實作者也是政策A1、A2、B1、B2 的分發者,這樣的節點隻需要
 * 同時繼承 {@code StrategyHandler} 和實作 {@code AbstractStrategyRouter} 接口就可以了。
 *
 * <pre>
 *           +---------+
 *           |  Root   |   ----------- 第 1 層政策入口
 *           +---------+
 *            /       \  ------------- 根據入參 P1 進行政策分發
 *           /         \
 *     +------+      +------+
 *     |  A   |      |  B   |  ------- 第 2 層不同政策的實作
 *     +------+      +------+
 *       /  \          /  \  --------- 根據入參 P2 進行政策分發
 *      /    \        /    \
 *   +---+  +---+  +---+  +---+
 *   |A1 |  |A2 |  |B1 |  |B2 |  ----- 第 3 層不同政策的實作
 *   +---+  +---+  +---+  +---+
 * </pre>
 *
 * @author: 尋弈
 * @date: 2020/4/16 2:46 下午
 * @see StrategyHandler
 */
@Component
public abstract class AbstractStrategyRouter<T, R> {

    /**
     * 政策映射器,根據指定的入參路由到對應的政策處理者。
     *
     * @param <T> 政策的入參類型
     * @param <R> 政策的傳回值類型
     */
    public interface StrategyMapper<T, R> {
        /**
         * 根據入參擷取到對應的政策處理者。可通過 if-else 實作,也可通過 Map 實作。
         *
         * @param param 入參
         * @return 政策處理者
         */
        StrategyHandler<T, R> get(T param);
    }

    private StrategyMapper<T, R> strategyMapper;

    /**
     * 類初始化時注冊分發政策 Mapper
     */
    @PostConstruct
    private void abstractInit() {
        strategyMapper = registerStrategyMapper();
        Objects.requireNonNull(strategyMapper, "strategyMapper cannot be null");
    }

    @Getter
    @Setter
    @SuppressWarnings("unchecked")
    private StrategyHandler<T, R> defaultStrategyHandler = StrategyHandler.DEFAULT;

    /**
     * 執行政策,架構會自動根據政策分發至下遊的 Handler 進行處理
     *
     * @param param 入參
     * @return 下遊執行者給出的傳回值
     */
    public R applyStrategy(T param) {
        final StrategyHandler<T, R> strategyHandler = strategyMapper.get(param);
        if (strategyHandler != null) {
            return strategyHandler.apply(param);
        }

        return defaultStrategyHandler.apply(param);
    }

    /**
     * 抽象方法,需要子類實作政策的分發邏輯
     *
     * @return 分發邏輯 Mapper 對象
     */
    protected abstract StrategyMapper<T, R> registerStrategyMapper();
}
           

繼承

AbstractStrategyRouter<T, R>

抽象類隻需要實作

protected abstract StrategyMapper<T, R> registerStrategyMapper();

抽象方法即可,在該方法中實作其不同子節點的路由邏輯。

如果子節點路由邏輯比較簡單,可以直接通過 if-else 進行分發。當然如果為了更好地性能、适應更複雜的分發邏輯也可以使用 Map 等儲存映射。

對于實作了該抽象類的 Router 節點,隻需要調用其

public R applyStrategy(T param)

方法即可擷取該節點的期望輸出。架構會自動根據定義的路由邏輯将 param 傳遞到對應的子節點,再由子節點不斷向下分發直到葉子節點或可以給出業務輸出的一層。這個過程有點類似遞歸或者分治的思想。

3.2 StrategyHandler 接口

/**
 * @author: 尋弈
 * @date: 2020/4/16 2:46 下午
 */
public interface StrategyHandler<T, R> {

    @SuppressWarnings("rawtypes")
    StrategyHandler DEFAULT = t -> null;

    /**
     * apply strategy
     *
     * @param param
     * @return
     */
    R apply(T param);
}
           

除了根節點外,都要實作

StrategyHandler<T, R>

接口。如果是葉子節點,由于不需要再向下委托,是以不再需要同時繼承

AbstractStrategyRouter<T, R>

抽象類,隻需要在

R apply(T param);

中實作業務邏輯即可。

對于其他責任樹中的中間層節點,都需要同時繼承 Router 抽象類和實作 Handler 接口,在

R apply(T param);

方法中首先進行一定異常入參攔截,遵循 fail-fast 原則,避免将這一層可以攔截的錯誤傳遞到下一層,同時也要避免「越權」做非本層職責的攔截校驗,避免産生耦合,為後面業務拓展挖坑。在攔截邏輯後直接調用本身 Router 的

public R applyStrategy(T param)

方法路由給下遊節點即可。

5 完結撒花

至此,關于如何通過「責任樹模式」優化這個需求場景的介紹就基本結束了,這不是一個複雜的需求,更不是一個多麼精妙的優化,這隻是日常需求開發中通過設計模式優化代碼的一個小例子。

最後再簡單聊聊我在日常需求開發過程中關于架構設計部分的一些思考。

其實并不是說用「if-else」很 Low,用設計模式就 Niubility,二者各有其擅長的應用場景,在合适的場景使用合适的代碼才是正道。其實「if-else」足以滿足大部分日常需求的開發,且簡單、靈活、可靠。這裡的「if-else」泛指樸素直白的程式設計模式,僅以實作需求業務功能為目的的編碼方式。當然,有些同學不滿足于此,希望可以通過經過思考的、更優的架構設計使代碼變的更簡潔、拓展性更好、性能更優、可讀性更好等等。不過對于此也存在反對的論述,謂之「過早優化乃萬惡之源」。

這句話源自 Donald Knuth 老人家:

曾夢想 if-else 走天涯?看看“責任樹模式”優化1 問題背景2 解決思路3 優化收益4 抽象架構5 完結撒花

這句話我當然承認其正确性,但我同樣覺得需要注意以下幾點:

  1. 任何「結論」都有其所處背景、上下文細節等,通過一句話指導工作是不成立的。優秀的架構師可以給出架構設計是在理論基礎、大量實踐、不斷思考總結以及無數采坑的經驗的基礎上得來的,而不是他知道一句别人都不知道的「咒語」;
  2. Knuth 這句話更偏重于反對奇技淫巧、細枝末節的性能優化,因為在「過早」的時候無法準确獲知系統的瓶頸且局部的優化不僅不能帶來收益,反而會造成更大的代價。他批評的恰恰是不着眼于整體架構的局部視角對系統的破壞,而架構設計正是需要從整體視角去做選擇與權衡。是以将 Knuth 這句話直接推廣到「架構設計」上并不妥當;
  3. 很多人覺得在項目開發時需求經常「瞬息萬變」、「朝令夕改」,而做優化又需要花費大量時間思考,根本沒有精力優化。我認為這種論述也是不成立的,憑什麼認為等到壞味道嚴重、曆史包袱沉重的時候就有精力、能力和膽量做優化了呢?
  4. 何時是所謂的「不早」很難界定,其實我們永遠都無法确定自己掌握了足夠的細節可以進行絕對正确的優化。在現實世界中,受到時間次元的限制,我們永遠無法達成全局最優,隻能以局部最優不斷去逼近全局最優。我覺得等到壞味道嚴重不得不重構的時候才想起優化已為時過晚;
  5. 這句話不應該成為不做設計的借口,即使最終送出的代碼仍是「if-else」版本,也不應省略思考、推演、權衡的過程,日常需求是練兵場,是精進技術的必經之路;

是以,我覺得不要被這句話束縛手腳,當然更不要閉門造車,在開發過程中勤于思考,向更有經驗的人請教,在架構設計上不斷學習、探索,才能擺脫日複一日通過「if-else」堆砌業務邏輯的循環。

繼續閱讀