設計模式-行爲模式之Chain-Of-Responsibility

更多請移步我的博客

意圖

責任鏈(Chain Of Responsibility)是一種行爲模式,通過給多個對象一個機會去處理請求的的方式來避免請求發送者和接受者的耦合。責任鏈接收對象並且沿着鏈條傳遞它,直到一個對象來處理它。

問題

假設你在做一個訂單系統。你第一個任務就是限制用戶對系統的訪問,只有已經授權的用戶可以創建訂單。另外,一些用戶擁有管理員權限,可以訪問全部的訂單。

你意識到這些檢查必須順序處理。程序能夠在任何時候對用戶進行嘗試認證,只要用戶的證書被請求傳遞。但是,如果未能對用戶進行身份驗證,則無法檢查用戶的權限。

幾個月後,你已經實現了這些順序檢查。

  • 你的同時建議,將原始數據直接交給代碼處理不安全。所以你添加了一個額外校驗步驟來驗證請求數據。

  • 之後,其他人建議協同無法應對暴力的密碼破解。爲了解決這個問題,你添加了另一個檢查來過濾重複使用相同用戶名但是失敗的請求。

  • 你甚至添加了緩存來提升訂單在高負載下的性能。

不幸的是,隨着新特性的增加,代碼變得越來越臃腫。甚至,你爲了保護其他頁面,把這些檢查代碼的一部分做了拷貝,導致出現重複代碼片段。

這個系統變得難以維護。但是,一天你收到了重構系統的任務…

解決

和其他行爲模式很像,責任鏈依賴於將行爲轉化爲獨立的對象。在我們的例子中,每個檢查都將被移動到不同的類中,這些類只有一個方法來執行這些檢查。這個方法通過參數來接收請求數據。

現在,到了有趣的部分。模式建議連接這些對象到一個鏈條中。 每個處理者都有一個字段來存儲這個鏈條中下一個處理者的引用。不管什麼時候,一個處理者接收到一個請求,它可以把請求傳遞給鏈條中在其之後的處理者。這個請求沿着鏈條旅行知道所有的處理者都有機會來處理它。

最後,一個處理者不需要在繼續傳遞這個請求。有兩種流行的方式來處理這個想法。

在我們訪問過濾的例子中,處理者排隊並挨個處理他們的檢查。流的終點只可能是某個檢查失敗或者到達鏈條尾部。

但還有一個略微不同的方式,處理者只會傳遞那些它們自己無法處理的請求。否則,它們執行自己的業務並且終止鏈條的執行。這個選項在處理GUI組件事件時很常見。

比如,當用戶點擊一個按鈕,這個事件便沿着由按鈕開始的組件鏈傳播,傳到他們的父組件,像form和panel,直到應用的窗口停止。這個事件被鏈條中第一個可以處理它的組件處理。這個例子值得一提,因爲它告訴我們一個鏈條可以從一個樹結構中抽離出來。

所有的處理者類都需要遵循一樣的接口。這將讓你可以在運行時使用各個處理者組合一個鏈條,而代碼不會和處理者的具體實現類耦合。每個具體的處理者應該只關心自己的execute方法。

現實世界的類比

技術支持

你爲你的PC買了一個新的顯卡。Windows可以自動檢測並啓用他。但你鍾愛的Linux卻無法使用新硬件。抱着微小的希望,你打電話給技術支持。

首先,你聽到了自動應答的機器人聲音。它提出了九種解決各種問題的流行解決方案,但沒有一個與你的問題有關。過了一會,機器把你轉給在線客服。

客服也沒有給出有用的解決方法,於是你請求聯繫一個正確的工程師。

客服把你轉給了工程師。最後,這個工程師告訴你到哪下載顯卡驅動及如何在Linux中怎麼安裝。於是,你愉快的結束了這通電話。

結構

structure.png

  1. Handler爲所有具體的處理者聲明一個通用接口。通常,它只有一個處理請求的方法,但有時候也會有設置鏈條下一個處理者的方法。

  2. Base Handler是可選的類,它可以包含負責構建維護對象責任鏈的模版代碼。

    這個類可以包含一個字段來存儲鏈條中下一個處理者。使用這個字段,客戶端可以將多個處理者鏈接到一個鏈中。這個字段可以通過構造方法或者一個set方法來控制。這個類也可能有一個基本處理方法的實現,這個方法檢查下一個處理者是否存在然後把執行傳遞給它。

  3. Concrete Handlers包含處理請求的實際代碼。接收到請求後,處理者必須決定是否處理該請求,另外還要決定是否在鏈條中繼續傳遞它。

    處理者通常是獨立的和不可變的,通過構造函數參數一次性接收所有必要的數據。

  4. Client可以只組裝一次鏈條或者依賴於程序邏輯動態組裝。注意,一個請求可以被髮送到鏈中的任何處理者,它並不總必須是第一個。

僞代碼

在這個例子中,責任鏈負責顯示活動UI元素關聯的一個上下文幫助。

這些GUI元素是樹狀結構。Dialog類渲染樹根的主窗口。中間層由Panel組成。葉子結點有:Component、Button、TextEdit等。

一個Component能夠顯示上下文提示,只要它有幫助文本。一些複雜的組件有他們自己的方式來顯示上下文幫助。

當用戶將鼠標光標指向組件並按下F1時,應用抓取這個組件併發送幫助請求。這個請求向上傳遞給所有父容器知道這個組件可以顯示幫助。

// Handler interface.
interface ComponentWithContextualHelp is
    method showHelp() is


// Base class for simple components.
abstract class Component implements ContextualHelp is
    field tooltipText: string

    // Container, which contains component, severs as a following object
    // in chain.
    protected field container: Container

    // Component shows tooltip if there is a help text assigned to it. Otherwise
    // it forwards the call to the container if it exists.
    method showHelp() is
        if (tooltipText != null)
            Show tooltip.
        else
            container.showHelp()


// Containers can contain both simple components and other container as
// children. The chain relations are established here. The class inherits
// showHelp behavior from its parent.
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// Primitive components may be fine with default help implementation...
class Button extends Component is
    // ...

// But complex components may override the default implementation. If a help can
// not be provided in a new way, the component can always call the base
// implementation (see Component class).
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            Show modal window with a help text.
        else
            parent::showHelp()

// ...same as above...
class Dialog extends Container is
    field wikiPage: string

    method showHelp() is
        if (wikiPage != null)
            Open a wiki help page.
        else
            parent::showHelp()


// Client code.
class Application is
    // Each application configures the chain differently.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPage = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is a OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // Imagine what happens here.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

適用性

  • 當一個程序有幾個處理不同請求的處理者,但事先並不知道過來的是什麼類型的請求。

    你要把幾個處理者放到一個鏈條中。請求沿着鏈條傳遞直到有個處理者能夠處理它。

  • 當要以特定順序執行處理程序。

    責任鏈允許按照給定的順序依次執行處理者。

  • 當有很多對象來處理請求,並且它們的順序動態改變。

    責任鏈允許對一個存在的鏈條中的處理者進行新增、移除或者排序操作。

如何實現

  1. 聲明Handler接口,包含一個處理請求的方法。決定如何傳遞請求的信息到方法中。最靈活的方法是將請求數據轉換爲對象並傳遞給處理方法。

  2. 爲了減少重複的樣板代碼,從Handler接口派生出一個抽象BaseHandler類是很值得的。

    添加一個字段來保存下一個處理者的引用。這個字段可以從構造參數中獲得初始化數據。你也可以定義一個set方法來修改這個字段。但僅當你想要在運行時想要修改鏈條才需要這麼做。

    實現處理方法,以便將請求轉發給鏈中的下一個對象(如果有的話)。具體的處理者能夠通過調用父類方法來轉發請求。因此,它們不需要訪問引用字段,你就可以把它聲明爲private了。

  3. 創建ConcreteHandler子類並且實現它們的處理方法。每個處理者在收到請求時應做出兩個決定:

    • 是否要處理這個請求。
    • 是否要繼續傳遞這個請求。
  4. Client可以自己組裝鏈條也可以從其他對象接收已經構造好的鏈條。在後一種情況下,可以採用工廠對象通過應用配置護着環境變量來構建鏈條。

  5. Client可能觸發鏈條中任意一個處理者,不僅僅只是第一個。這個調用將會沿着鏈條傳遞直到鏈條結尾或者一些處理者拒絕進一步傳遞它。

  6. 由於鏈條的動態特性,Client應該準備好處理以下情況:

    • 有時一個鏈可能包含一個單一的鏈接。
    • 一些請求可能無法到達鏈條的尾部。
    • 一些請求到達鏈條尾部還未被處理。

優點

  • 減少請求發送者和接受者的耦合。

  • 遵循單一職責原則。

  • 遵循開閉原則。

缺點

  • 一些請求肯能到鏈條尾部仍未被處理。

和其他模式的關係

  • Chain Of Responsibility,Command,Mediator和Observer處理連接請求的發送者和接收者的各種方式:

    • 責任鏈沿着潛在接收者的動態鏈順序傳遞一個請求,直到其中一個處理這個請求。

    • 命令模式建立從發送者到接收者的單向連接。

    • 調解模式持有發送者和接收者間接引用。

    • 觀察者會在同一時間把一個請求發送給所有關心的接受者,但是允許它們動態的確定是否繼續訂閱和取消訂閱後面的請求。

  • 責任鏈通常和組合(Composite)結合使用。在這種情況下,一個組件的父類可以看作是他的後繼者。

  • 責任鏈中的處理者可以表示爲命令(Command)。在這種情況下,許多不同的操作可以在由請求表示的相同上下文中執行。

    但還有另外一種方式,請求本身就是一個Command對象,沿着對象鏈傳遞。這種情況下,相同的操作可以在由鏈條對象表示的不同上下文中執行。

  • 責任鏈和裝飾者(Decorator)的類結構很相似。它們都依賴於遞歸組合來在一系列對象中傳遞執行。但是它們也有幾個關鍵區別。

    責任鏈的處理者能夠隨意執行動作,之間相互獨立。它們也能夠隨意終止請求的進一步傳遞。另一方面,各個裝飾者擴展一個特定行爲並應該保持其接口一致。另外,裝飾者不允許隨意中斷鏈條的執行。

小結

責任鏈允許請求煙盒潛在的處理鏈傳遞直到某個處理者處理這個請求。這種模式允許多個對象處理這個請求而不發送者類不需要和具體接受者類耦合。這個鏈可以在運行時動態組合遵循標準處理者接口的任何處理者。

在Java中比較流行的使用樣例:在GUI類中向父組件傳遞事件;過濾訪問請求。

在Java的核心類庫中有些例子:

  • javax.servlet.Filter#doFilter()
  • java.util.logging.Logger#log()

當我們發現組織結構類似以下描述時,可能就採用了責任鏈模式:一個對象的行爲方法間接調用其他對象中的相同方法,而所有對象都遵循共同的接口。

參考

翻譯整理自:https://refactoring.guru/design-patterns/chain-of-responsibility

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章