說到責任鏈設計模式, 我們平時使用的也真是挺多的. 比如: 天天用的閘道器過濾器, 我們請假的審批流, 打遊戲通關, 我們寫程式碼常用的日誌列印. 他們都使用了責任鏈設計模式.
下面就來詳細研究一下責任鏈設計模式
一. 什麼是責任鏈設計模式?
官方定義:
責任鏈模式(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) {
// 總經理審批
}
}
這樣寫確實可以實現。 但是他有幾個缺點:
- 這個審批方法很長,一大段程式碼看起來並不美觀。 這裡看著程式碼很少,那是因為我沒有具體實現審批邏輯, 當審批人很多的時候, if...else...也會很多,就會顯得很臃腫了。
- 可擴充套件性差: 加入現在要在部門經理和總經理之間在家一個審批流。 我們要修改原來的程式碼,修改原來的程式碼,就有可能引入bug, 違背了開放-封閉原則。
- 違背單一職責原則:這個類承擔了多個角色的多個責任,違背了單一職責原則。
- 不能跨級別審批:加入有一個特殊的人,他請假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);
}
這裡定義瞭如下內容:
- 審批者姓名,
- 審批人要執行的操作process()方法。審批的內容是請假資訊, 返回值是審批結果,通過或者不通過
- 下一個處理者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("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");
}
}
我們在客戶端只需要直接呼叫第一個閘道器處理器就可以了, 不需要關心其他的處理器.
五. 責任鏈模式總結
- 定義一個抽象的父類, 在抽象的父類中定義請求處理的方法 和 下一個處理者.
- 然後子類處理器繼承分類處理器, 並實現自己的請求處理方法
- 設定處理請求鏈, 可以採用工廠設計模式抽象, 請求者只需要知道整個鏈條的第一環