23種設計模式(六)-責任鏈設計模式

盛開的太陽發表於2021-06-25

說到責任鏈設計模式, 我們平時使用的也真是挺多的. 比如: 天天用的閘道器過濾器, 我們請假的審批流, 打遊戲通關, 我們寫程式碼常用的日誌列印. 他們都使用了責任鏈設計模式.

下面就來詳細研究一下責任鏈設計模式

一. 什麼是責任鏈設計模式?

官方定義:


責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。這種模式給予請求的型別,對請求的傳送者和接收者進行解耦。這種型別的設計模式屬於行為型模式。

在這種模式中,通常每個接收者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

大白話:


定義中提到的兩個主體: 請求的傳送者和請求的接收者. 用員工請假來舉例. 請求傳送者是員工, 請求接收者是主管們.
「對請求的傳送者和接收者進行解耦」: 意思就是員工發起請假申請和主管審批請假解耦.
「為請求建立了一個接收者物件的鏈」: 意思是接收者有多個, 實現了多個接收者進行審批的鏈條.

二. 責任鏈設計模式的使用場景

  • 閘道器過濾器: 一個url請求過來, 首先要校驗url是否是合法的, 不合法過濾掉, 合法進入下一層校驗; 是否是在黑名單中, 如果在過濾掉,不在進行下一層校驗; 校驗引數是否合規, 不合規過濾掉, 合規進入下一層校驗, 等等.
  • 請假審批流: 請假天數小於3天, 直屬領導審批即可; 天數大於3天,小於10天, 要部門主管審批; 天數大於10天要總經理審批
  • 遊戲通關: 完成第一關, 並且分數>90, 才能進入第二關; 完成第二關, 分數>80, 才能進入第三關等等
  • 日誌處理: 日誌的級別從小到大分別是: dubug, info ,warn, error .
    • console控制檯: 控制檯接收debug級別的日誌, 那麼所有debug, info, warn, error日誌內容都列印在console控制檯中.
    • file檔案: file接收info級別的日誌. 那麼info, warn, error級別的日誌都會列印到file檔案中, 但是debug日誌不會列印
    • error檔案: 只接收error級別的日誌, 其他界別的日誌都不接收.

三. 責任鏈設計模式的實現思路

下面以一個簡單的案例[請假審批流]來介紹責任鏈的實現

1. 需求:

有一個員工小力, 他要請求. 公司規定, 請假3天以內, 直屬領導就可以審批. 請假3-10天, 需要部門經理審批. 請假大於10天需要總經理審批.

2. 通常實現方式

這個審批流, 我們第一想法是使用if....else....來寫.

public void approve(Integer days) {
    if (days <= 3) {
        // 直屬領導審批
    } else if (days > 3 && days <= 10) {
        // 部門經理審批
    } else if (days > 10) {
        // 總經理審批
    }
}

這樣寫確實可以實現。 但是他有幾個缺點:

  1. 這個審批方法很長,一大段程式碼看起來並不美觀。 這裡看著程式碼很少,那是因為我沒有具體實現審批邏輯, 當審批人很多的時候, if...else...也會很多,就會顯得很臃腫了。
  2. 可擴充套件性差: 加入現在要在部門經理和總經理之間在家一個審批流。 我們要修改原來的程式碼,修改原來的程式碼,就有可能引入bug, 違背了開放-封閉原則。
  3. 違背單一職責原則:這個類承擔了多個角色的多個責任,違背了單一職責原則。
  4. 不能跨級別審批:加入有一個特殊的人,他請假3天,也需要總經理審批,這個if...else....就沒法實現了。

既然可能增加多個審批人,我們可以考慮將具體的審批人做成審批者的子類,利用多型來實現。

3. 責任鏈實現方式

第一步: 小力請假, 定義一個請假實體類LeaveRequest。這就是請求的發出者

@Data
public class LeaveRequest {
    /**
     * 請假的人
     */
    private String name;

    /**
     * 請假的天數
     */
    private int days;

    public LeaveRequest() {
    }

    public LeaveRequest(String name, int days) {
        this.name = name;
        this.days = days;
    }
}

有兩個屬性, 誰請假(name), 請了幾天(days).

第二步: 抽象請假審批者

/**
 * 抽象的請假處理類
 */
@Data
public abstract class LeaveHandler {
    /**
     * 處理人姓名
     */
    private String handlerName;

    /**
     * 下一個處理人
     */
    private LeaveHandler nextHandler;

    public void setNextHandler(LeaveHandler leaveHandler) {
        this.nextHandler = leaveHandler;
    }

    public LeaveHandler(String handlerName) {
        this.handlerName = handlerName;
    }

    /**
     * 具體的處理操作
     * @param leaveRequest
     * @return
     */
    public abstract boolean process(LeaveRequest leaveRequest);
}

這裡定義瞭如下內容:

  1. 審批者姓名,
  2. 審批人要執行的操作process()方法。審批的內容是請假資訊, 返回值是審批結果,通過或者不通過
  3. 下一個處理者nextHandler:這是重點。也是我們鏈條能夠連續執行的關鍵。

第三步:定義具體的操作者

  • 直屬領導處理類:DirectLeaveHandler.java
/**
 * 天數小於3天, 直屬領導處理
 */
public class DirectLeaveHandler extends LeaveHandler{
    public DirectLeaveHandler(String directName) {
        super(directName);
    }
    @Override
    public boolean process(LeaveRequest leaveRequest) {
        // 隨機數大於3則為批准,否則不批准
        boolean result = (new Random().nextInt(10)) > 3;
        if (!result) {
            System.out.println(this.getHandlerName() + "審批駁回");
            return false;
        } else if (leaveRequest.getDays() <= 3) {
            // 審批通過
            System.out.println(this.getHandlerName() + "審批完成");
            return true;
        } else{
            System.out.println(this.getHandlerName() + "審批完成");
            return this.getNextHandler().process(leaveRequest);
        }
    }
}

這裡模擬了領導審批的流程. 如果小於3天, 直屬領導直接審批, 可能通過, 可能不通過. 如果超過3天, 提交給下一級領導審批.

  • 部門經理處理類: ManagerLeaveHandler
public class ManagerLeaveHandler extends LeaveHandler{

    public ManagerLeaveHandler(String name) {
        super(name);
    }
    @Override
    public boolean process(LeaveRequest leaveRequest) {
        // 隨機數大於3則為批准,否則不批准
        boolean result = (new Random().nextInt(10)) > 3;
        if (!result) {
            System.out.println(this.getHandlerName() + "審批駁回");
            return false;
        } else if (leaveRequest.getDays() > 3 && leaveRequest.getDays() <= 10) {
            System.out.println(this.getHandlerName() + "審批完成");
            return true;
        } else {
            System.out.println(this.getHandlerName() + "審批完成");
            return this.getNextHandler().process(leaveRequest);
        }
    }
}

部門經理處理的是3-10天的假期, 如果超過10天, 還要交由下一級領導審批
** 總經理處理類:

public class GeneralManagerLeavHandler extends LeaveHandler{
    public GeneralManagerLeavHandler(String name) {
        super(name);
    }
    @Override
    public boolean process(LeaveRequest leaveRequest) {
        // 隨機數大於3則為批准,否則不批准
        boolean result = (new Random().nextInt(10)) > 3;
        if (!result) {
            System.out.println(this.getHandlerName() + "審批駁回");
            return false;
        } else {
            System.out.println(this.getHandlerName() + "審批完成");
            return true;
        }
    }
}

左右最終流轉到總經理的假期都會被審批

第四步: 定義客戶端發起請求操作

    public static void main(String[] args) {
        DirectLeaveHandler directLeaveHandler = new DirectLeaveHandler("直屬主管");
        ManagerLeaveHandler managerLeaveHandler = new ManagerLeaveHandler("部門經理");
        GeneralManagerLeavHandler generalManagerLeavHandler = new GeneralManagerLeavHandler("總經理");

        directLeaveHandler.setNextHandler(managerLeaveHandler);
        managerLeaveHandler.setNextHandler(generalManagerLeavHandler);

        System.out.println("========張三請假2天==========");
        LeaveRequest lxl = new LeaveRequest("張三", 2);
        directLeaveHandler.process(lxl);
        
        System.out.println("========李四請假6天==========");
        LeaveRequest wangxiao = new LeaveRequest("李四", 6);
        directLeaveHandler.process(wangxiao);


        System.out.println("========王五請假30天==========");
        LeaveRequest yongMing = new LeaveRequest("王五", 30);
        directLeaveHandler.process(yongMing);
    }

這裡我們建立了一個直屬領導, 一個部門經理,一個總經理. 並設定了上下級關係.
然後根據員工請假的天數來判斷, 應該如何審批.
對於使用者而言,他不需要知道前面有多少個領導需要審批. 他只需要提交給第一個領導, 也就是直屬領導, 然後不斷往下走審批就可以了. 也就是說,在責任鏈設計模式中,我們只需要拿到鏈上的第一個處理者,那麼鏈上的每個處理者都有機會處理相應的請求。

以上程式碼基本上概括了責任鏈設計模式的使用,但是上述客戶端的程式碼其實也是很繁瑣的,後面我們會繼續優化責任鏈設計模式。

第五步: 檢視結果

由於請假是隨機了, 還有可能被駁回. 我們先來看看全部同意的請求結果

========張三請假2天==========
直屬主管審批完成
========李四請假6天==========
直屬主管審批完成
部門經理審批完成
========王五請假30天==========
直屬主管審批完成
部門經理審批完成
總經理審批完成

再來看看有駁回的請求結果

========張三請假2天==========
直屬主管審批駁回
========李四請假6天==========
直屬主管審批駁回
========王五請假30天==========
直屬主管審批完成
部門經理審批駁回

4. 責任鏈概念抽象總結

23種設計模式(六)-責任鏈設計模式

責任鏈設計模式: 客戶端發出一個請求,鏈上的物件都有機會來處理這一請求,而客戶端不需要知道誰是具體的處理物件。多個物件都有機會處理請求,從而避免了請求的傳送者和接受者之間的耦合關係。 將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有物件處理它為止

上面的程式碼基本上概括了責任鏈設計模式的使用,但是上述客戶端的程式碼其實也是很繁瑣的,後面我優化責任鏈設計模式。

4. 責任鏈設計模式的優缺點

優點

動態組合,使請求者和接受者解耦。
請求者和接受者鬆散耦合:請求者不需要知道接受者,也不需要知道如何處理。每個職責物件只負責自己的職責範圍,其他的交給後繼者。各個元件間完全解耦。
動態組合職責:職責鏈模式會把功能分散到單獨的職責物件中,然後在使用時動態的組合形成鏈,從而可以靈活的分配職責物件,也可以靈活的新增改變物件職責。

缺點

產生很多細粒度的物件:因為功能處理都分散到了單獨的職責物件中,每個物件功能單一,要把整個流程處理完,需要很多的職責物件,會產生大量的細粒度職責物件。
不一定能處理:每個職責物件都只負責自己的部分,這樣就可以出現某個請求,即使把整個鏈走完,都沒有職責物件處理它。這就需要提供預設處理,並且注意構造鏈的有效性。

四. 綜合案例 -- 閘道器許可權控制

1. 明確需求

閘道器有很多功能: API介面限流, 黑名單攔截, 許可權驗證, 引數過濾等. 下面我們就通過責任鏈設計模式來實現閘道器許可權控制。

2. 實現思路

來看一下下面的類圖.
23種設計模式(六)-責任鏈設計模式

可以看到定義了一個抽象的閘道器處理器. 然後有4個子處理器的實現類.

3. 具體實現

第一步: 定義抽象的閘道器處理器類

/**
 * 定義抽象的閘道器處理器類
 */
public abstract class AbstractGatewayHandler {
    /**
     * 定義下一個閘道器處理器
     */
    protected AbstractGatewayHandler nextGatewayHandler;

    public void setNextGatewayHandler(AbstractGatewayHandler nextGatewayHandler) {
        this.nextGatewayHandler = nextGatewayHandler;
    }

    /**
     * 抽象閘道器執行的服務
     * @param url
     */
    public abstract void service(String url);
}

第二步: 定義具體的閘道器服務

1. API介面限流處理器

/**
 * API介面限流處理器
 */
public class APILimitGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("api介面限流處理, 處理完成");
        // 實現具體的限流服務流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

2. 黑名單攔截處理器

/**
 * 黑名單處理器
 */
public class BlankListGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("黑名單處理, 處理完成");

        // 實現具體的限流服務流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

3. 許可權驗證處理器

/**
 * 許可權驗證處理器
 */
public class PermissionValidationGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("許可權驗證處理, 處理完成");
        // 實現具體的限流服務流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

4. 引數校驗處理器

/**
 * 引數校驗處理器
 */
public class ParameterVerificationGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("引數校驗處理, 處理完成");
        // 實現具體的限流服務流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

第三步: 定義閘道器客戶端, 設定閘道器請求鏈

/**
 * 閘道器客戶端
 */
public class GatewayClient {
    public static void main(String[] args) {
        APILimitGatewayHandler apiLimitGatewayHandler = new APILimitGatewayHandler();
        BlankListGatewayHandler blankListGatewayHandler = new BlankListGatewayHandler();
        ParameterVerificationGatewayHandler parameterVerificationGatewayHandler = new ParameterVerificationGatewayHandler();
        PermissionValidationGatewayHandler permissionValidationGatewayHandler = new PermissionValidationGatewayHandler();

        apiLimitGatewayHandler.setNextGatewayHandler(blankListGatewayHandler);
        blankListGatewayHandler.setNextGatewayHandler(parameterVerificationGatewayHandler);
        parameterVerificationGatewayHandler.setNextGatewayHandler(permissionValidationGatewayHandler);
        
        apiLimitGatewayHandler.service("http://www.baidu.com");
    }
}

這裡和之前差不多, 不做太多解釋了, 來看執行效果:

api介面限流處理, 處理完成
黑名單處理, 處理完成
引數校驗處理, 處理完成
許可權驗證處理, 處理完成

這樣就進行了一系列的閘道器處理. 當然, 每一次處理都應該返回處理結果, 然後決定是否進行下一次處理. 這裡就簡化了

第四步: 使用工廠模式優化責任鏈設計模式

在第三步閘道器客戶端中,對責任鏈進行了初始化操作。 這樣, 每次客戶端想要發起請求都需要執行一遍初始化操作, 其實完全沒有這個必要. 我們可以使用工廠設計模式, 將客戶端抽取到工廠中, 每次只需要拿到鏈上的第一個處理者就可以了.

1. 定義閘道器處理器工廠

/**
 * 閘道器處理器工廠
 */
public class GatewayHandlerFactory {
    public static AbstractGatewayHandler getFirstGatewayHandler() {
        APILimitGatewayHandler apiLimitGatewayHandler = new APILimitGatewayHandler();
        BlankListGatewayHandler blankListGatewayHandler = new BlankListGatewayHandler();
        ParameterVerificationGatewayHandler parameterVerificationGatewayHandler = new ParameterVerificationGatewayHandler();
        PermissionValidationGatewayHandler permissionValidationGatewayHandler = new PermissionValidationGatewayHandler();

        apiLimitGatewayHandler.setNextGatewayHandler(blankListGatewayHandler);
        blankListGatewayHandler.setNextGatewayHandler(parameterVerificationGatewayHandler);
        parameterVerificationGatewayHandler.setNextGatewayHandler(permissionValidationGatewayHandler);

        return apiLimitGatewayHandler;
    }
}

閘道器處理器工廠定義了各個閘道器處理器之間的關係, 並返回第一個閘道器處理器.

2.優化閘道器客戶端

/**
 * 閘道器客戶端
 */
public class GatewayClient {
    public static void main(String[] args) {
        GatewayHandlerFactory.getFirstGatewayHandler().service("http://www.baidu.com");
    }
}

我們在客戶端只需要直接呼叫第一個閘道器處理器就可以了, 不需要關心其他的處理器.

五. 責任鏈模式總結

  1. 定義一個抽象的父類, 在抽象的父類中定義請求處理的方法 和 下一個處理者.
  2. 然後子類處理器繼承分類處理器, 並實現自己的請求處理方法
  3. 設定處理請求鏈, 可以採用工廠設計模式抽象, 請求者只需要知道整個鏈條的第一環

相關文章