淺析設計模式5 -- 責任鏈模式

技術聯盟總壇 發佈 2023-06-06T02:26:25.897876+00:00

劉文慧(鎏越) 大淘寶技術 2023-06-05 16:25 發表於浙江我們在進行軟體開發時要想實現可維護、可擴展,就需要儘量復用代碼,並且降低代碼的耦合度。設計模式就是一種可以提高代碼可復用性、可維護性、可擴展性以及可讀性的解決方案。

劉文慧(鎏越) 大淘寶技術 2023-06-05 16:25 發表於浙江




我們在進行軟體開發時要想實現可維護、可擴展,就需要儘量復用代碼,並且降低代碼的耦合度。設計模式就是一種可以提高代碼可復用性、可維護性、可擴展性以及可讀性的解決方案。大家熟知的23種設計模式,可以分為創建型模式、結構型模式和行為型模式三大類。本專題著眼於實際開發過程中常用的幾種設計模式,從理論和實戰兩個角度進行討論和分享,力求邏輯清晰、表述簡潔,幫助大家在項目中合理運用設計模式,保障代碼的可靠性。


概述


我們在進行軟體開發時要想實現可維護、可擴展,就需要儘量復用代碼,並且降低代碼的耦合度,而設計模式就是一種可以提高代碼可復用性、可維護性、可擴展性以及可讀性的解決方案。


大家熟知的23種設計模式,可以分為創建型模式、結構型模式和行為型模式三大類。其中,行為型模式可用於描述程序中多個類和多個對象如何協作完成複雜的任務,涉及不同對象間的職責分配、算法的抽象化。責任鏈模式是一種典型的行為型模式,本文將著眼於責任鏈模式進行學習分享,闡述責任鏈模式的核心概念和應用實踐。認識和使用責任鏈模式,可以在開發中有效增強應用的靈活性和可擴展性。



基本概念


責任鏈模式的核心思想是:將請求的每個處理者都視為一個處理節點,再將這些節點連成一條鏈,當請求到來後便可沿著這條鏈進行傳遞,直到有節點處理它為止。這種模式可以有效避免請求發送者和請求接受者之間的耦合關係,用戶只需要將請求發送到責任鏈上,不用關心傳遞過程和處理細節。


下面從模式結構和使用步驟兩個方面,簡單闡述責任鏈方法模式的基本概念。


▐ 結構


責任鏈模式的結構也比較簡單易懂,主要包含三大類:抽象處理者類、具體處理者類和客戶類,抽象處理者類中首先定義好抽象方法和後繼處理機制,具體的處理方法實現將在具體處理者中執行,而客戶類將對所有定義的具體處理者組裝成鏈,讓請求從鏈頭開始沿著責任鏈向後執行。


角色

關係

作用

抽象處理者 Handler

具體處理者的父類

定義一個處理請求的接口,包含抽象處理方法和一個後繼連接。

具體處理者 Concrete Handler

抽象構件的接口實現類

實現抽象處理者的抽象處理方法,判斷能否處理本次請求,如果可以則處理,否則將轉交給其後繼者。

客戶類 Client

將具體處理者組裝成責任鏈

創建處理鏈,並向鏈頭的具體處理者提交請求,它不關心處理細節和請求的傳遞過程。



▐ 使用


基於上述基本概念,將裝飾者模式的使用步驟概括為:

step1:創建抽象處理者類,定義一個抽象方法和一個指向下一處理者節點的指針 next。

step2:創建具體處理者類,實現抽象處理者類中定義的抽象方法;

step3:創建客戶類,將各個具體處理者類組裝成責任鏈,將請求提交給鏈頭的具體處理者類。


使用示例


其實,大家平常接觸到的各種審批流,就涉及多個審批節點,不同的審批節點負責人分別持有不同級別的審批權限。比如:請假審批流、緊急發布審批流、報銷審批流等等。疫情管控時期,我們學校為了確保在校師生及其他員工的安全,會嚴格要求學生不能隨意出入校門,如果有特殊情況如看病就醫,則需在校務系統中進行申請,系統會根據學生請假出校的時長,設置不同級別的負責人進行嚴格審批。這一過程,用責任鏈模式就可以實現。


▐ 代碼實現


// 創建抽象處理者類,定義指向下一節點的指針和抽象處理方法
public abstract class AbstractHandler {
    private AbstractHandler next;
    public void setNext(AbstractHandler next) {
        this.next = next;
    }
    public AbstractHandler getNext() {
        return next;
    }
    public abstract void handlerequest(int leaveDayNum);
}


// 定義具體處理者類1:系統自動審批
public class ConcreteHandler1 extends AbstractHandler {
    @Override
    public void handleRequest(int leaveDayNum) {
        if (leaveDayNum <= 1) {
            System.out.println("請假不超過" + leaveDayNum + "天" + ": 學校自動審批通過");
        } else {
            if (getNext() != null) {
                getNext().handleRequest(leaveDayNum);
            } else {
                System.out.println("請假天數過長,需向學院提供簽字承諾書");
            }
        }
    }
}


// 定義具體處理中類2:導師審批
public class ConcreteHandler2 extends AbstractHandler {
    @Override
    public void handleRequest(int leaveDayNum) {
        if (leaveDayNum <= 3) {
            System.out.println("請假不超過" + leaveDayNum + "天" + ": 導師審批通過");
        } else {
            if (getNext() != null) {
                getNext().handleRequest(leaveDayNum);
            } else {
                System.out.println("請假天數過長,需向學院提供簽字承諾書");
            }
        }
    }
}


// 定義具體處理者類3:輔導員審批
public class ConcreteHandler3 extends AbstractHandler {
    @Override
    public void handleRequest(int leaveDayNum) {
        if (leaveDayNum <= 5) {
            System.out.println("請假不超過" + leaveDayNum + "天" + ": 輔導員審批通過");
        } else {
            if (getNext() != null) {
                getNext().handleRequest(leaveDayNum);
            } else {
                System.out.println("請假天數過長,需向學院提供簽字承諾書");
            }
        }
    }
}


// 定義具體處理者類4:院長審批
public class ConcreteHandler4 extends AbstractHandler {
    @Override
    public void handleRequest(int leaveDayNum) {
        if (leaveDayNum <= 7) {
            System.out.println("請假不超過" + leaveDayNum + "天" + ": 院長審批通過");
        } else {
            if (getNext() != null) {
                getNext().Handlerequest(leaveDayNum);
            } else {
                System.out.println("請假天數過長,需向學院提供簽字承諾書");
            }
        }
    }
}


//定義客戶類:組裝責任鏈,向鏈頭節點發送請求
public class chainOfResponsibility {
    public static void main(String[] args) {
        AbstractHandler handler1 = new ConcreteHandler1();
        AbstractHandler handler2 = new ConcreteHandler2();
        AbstractHandler handler3 = new ConcreteHandler3();
        AbstractHandler handler4 = new ConcreteHandler4();
        handler1.setNext(handler2);
        handler2.setNext(handler3);
        handler3.setNext(handler4);
        handler1.handleRequest(6);
    }
}


▐ 結果輸出


請假不超過6天: 院長審批通過


▐ UML圖



擴展


很多文章在介紹責任鏈模式時都會提到一個概念:純的責任鏈模式、不純的責任鏈模式,這裡也做一個簡單的擴展。

  1. 純的職責鏈模式:一個請求必須被某一個處理者對象所接收,且一個具體處理者對某個請求的處理只能採用以下兩種行為之一:自己處理(承擔責任);把責任推給下一個具體處理者處理。
  2. 不純的職責鏈模式:允許出現某一個具體處理者在承擔了請求的部分責任後,再將剩餘責任傳給下一個具體處理者繼續處理剩餘部分責任。


另外,在日常使用中,Handler 不是一定要提供一個設置後繼處理器的接口,可以把維護鏈路的職責獨立出來,如下圖所示。



在這種責任鏈模式中,HandlerChain 來維護整條鏈路,它提供了增刪處理者的方法,並實現了抽象處理者類的接口,負責在責任鏈中傳遞請求 Request。同時,HandlerChain 內部維護了當前具體處理者在整個責任鏈中的索引。當客戶類發出請求後,HandlerChain 會驅動第一個具體處理者(pos = 0),並根據需要判斷是否繼續向後傳遞,若需要則調用 handle() 方法再次驅動下一個具體處理者(pos 在上一次驅動時自增),若不需要則跳出鏈路;如此反覆,直到有具體處理者判斷不需再向後傳遞,或已經執行到最後一個具體處理者。


這樣,每個具體處理者不再依賴後繼處理器,而是各自在 HandlerChain 中的位置 pos,從而增強模式的靈活性,解除具體處理者之間的依賴性。servlet 中的過濾器 Filter 就是基於這種方式實現的。


源碼賞析



Servlet 約定在請求進入容器後、執行 Servlet.service() 方法前,可以通過 Filter 對 web 資源進行過濾、權限鑑別等處理。在 tomcat 啟動時,先加載所有的過濾器信息;在 tomcat 收到請求時,再加載並執行整個過濾器的鏈路,當請求從鏈路中脫離後,才會進入真正的業務接口,如下圖所示。



在 tomcat 中,每一個 Filter 都是一個具體處理者,不僅能處理請求、還能處理響應。對於 request 來說,責任鏈結構為 Filter1 -> Filter2 -> Filter3;而對於響應來說,責任鏈結構為 Filter3 -> Filter2 -> Filter1。這種雙向處理的思想其實也很經典,下面將簡單分析一下源碼。


public interface Filter {
    public default void init(FilterConfig filterConfig) throws ServletException {}
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;
    public default void destroy() {}
}


Filter 為抽象處理者,提供了三個方法,其中 doFilter() 方法為處理方法,三個參數分別為請求、響應和鏈路管理器。接下來看一下鏈路管理器 FilterChain 的源碼。


// FilterChain
public interface FilterChain {
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException;
}


// ApplicationFilterChain
public final class ApplicationFilterChain implements FilterChain {    
    // 處理器鏈路,ApplicationFilterConfig 可以理解為對 Filter 的包裝
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];   
    // 當前處理器在鏈路中的索引
    private int pos = 0;    
    // 調用入口
    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {
        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedExceptionAction<Void>() {
                        @Override
                        public Void run()
                            throws ServletException, IOException {
                            // here
                            internalDoFilter(req,res);
                            return null;
                        }
                    }
                );
            } catch( PrivilegedActionException pe) {
                Exception e = pe.getException();
                if (e instanceof ServletException)
                    throw (ServletException) e;
                else if (e instanceof IOException)
                    throw (IOException) e;
                else if (e instanceof RuntimeException)
                    throw (RuntimeException) e;
                else
                    throw new ServletException(e.getMessage(), e);
            }
        } else {
            // here
            internalDoFilter(request,response);
        }
    }
    
    // 開始處理
    private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {
        // 不停的驅動下一個過濾器
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();


                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();


                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    // 驅動過濾器
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.filter"), e);
            }
            return;
        }


        // 鏈路中沒有更多過濾器了,開始進入 servlet
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }
            if (request.isAsyncSupported() && !servletSupportsAsync) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
                // 進入servlet 處理實際業務
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(null);
                lastServicedResponse.set(null);
            }
        }
    }
}


ApplicationFilterChain 用數組來組織各處理者的先後順序,並提供一個當前處理者在鏈路中的索引 pos 。當鏈路未在中途斷開且當前處理者已是最後一個處理者時,調用 Servlet.service(request, response) 進入業務處理邏輯。作為用戶,我們可以通過配置文件或者注入等方式,根據需要定義新的 Filter。


優缺點及適用場景


▐ 優點


  1. 降低對象之間的耦合度。一個節點對象無須關心鏈的結構、到底是哪一個對象處理其請求,發送者和接收者也無須擁有對方的明確信息。
  2. 增強系統的可擴展性。可以根據需要增加新的請求處理類,滿足開閉原則。
  3. 靈活地為對象指派職責。當工作流程變化時,可動態改變節點或調動節點次序,也可動態增刪節點。
  4. 簡化節點之間的連接。各節點只需保持一個指向其後繼者的引用,避免使用眾多 if 或 if…else 語句。
  5. 責任分擔,符合類的單一職責原則。每個節點類只需處理自己該處理的工作,不該處理的傳遞給下一個節點類完成,各類的責任範圍非常明確。



▐ 缺點


  1. 不能保證每個請求一定被處理。一個請求沒有明確的接收者,可能一直傳到鏈的末端都得不到處理。
  2. 當責任鏈太長時,一個請求可能需要涉及多個處理者,系統性能會受到一定影響。
  3. 責任鏈建立的合理性需要由客戶端來保證,增加了客戶端的複雜性,也可能會因為錯誤設置而導致系統陷入死循環。


▐ 適用場景


  1. 在運行時需要動態使用多個關聯對象來處理同一次請求時。比如,請假流程、員工入職流程、編譯打包發布上線流程等。
  2. 不想讓使用者知道具體的處理邏輯時。比如,做權限校驗的登錄攔截器。
  3. 需要動態更換處理對象時。比如,工單處理系統、網關 API 過濾規則系統等。
  4. 職責鏈模式常被用在框架開發中,實現過濾器、攔截器等功能,使用者可以在不修改源碼的情況下,添加新的過濾攔截功能。


團隊介紹


我們是大聚划算技術團隊。負責支持聚划算、百億補貼、天天特賣、淘特價等業務。我們聚焦優惠和選購體驗,通過數智化驅動形成更有效率和確定性的貨品運營方法論,為消費者提供精選和極致性價比的商品,為商家提供更具爆發確定性的營銷方案。

關鍵字: