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

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

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

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

官方定義:

責任鏈模式(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. 責任鏈概念抽象總結

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

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

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

優點

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

缺點

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

四. 綜合案例 — 網關權限控制

1. 明確需求

網關有很多功能: API接口限流, 黑名單攔截, 權限驗證, 參數過濾等. 下面我們就通過責任鏈設計模式來實現網關權限控制。

2. 實現思路

來看一下下面的類圖.

可以看到定義了一個抽象的網關處理器. 然後有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("//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("//www.baidu.com");
    }
}

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

五. 責任鏈模式總結

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