職責鏈模式的定義是使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關系,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。職責鏈模式的名字非常形象,一系列可能會處理請求的對象被連接成一條鏈,請求在這些對象之間依次傳遞,直到遇到一個可以處理它的對象,把這些對象稱為鏈中的節點。
基本流程
職責連是由多個不同的對象組成的,有發送者跟接收者,分別負責信息的發送跟接收,其中,鏈中第一個對象是 職責連是由多個不同的對象組成的,發送者是發送請求的對象,接收者接收請求并且對其進行處理或傳遞的對象。基本流程如下:
- 發送者知道鏈中的第一個接收者,它向這個接收者發送該請求。
- 每一個接收者都對請求進行分析,然后要么處理它,要么它往下傳遞。
- 每一個接收者知道其他的對象只有一個,即它在鏈中的下家(successor)。
- 如果沒有任何接收者處理請求,那么請求會從鏈中離開。
職責鏈模式是個鏈式結構,請求在鏈中的節點之間依次傳遞,直到有一個對象能處理該請求為止。如果沒有任何對象處理該請求的話,那么請求就會從鏈中離開。
實戰
職責鏈模式的例子在現實中并不難找到,以下就是兩個常見的跟職責鏈模式有關的場景
如果早高峰能順利擠上公交車的話,那么估計這一天都會過得很開心。因為公交車上人實在太多了,經常上車后卻找不到售票員在哪,所以只好把兩塊錢硬幣往前面遞。除非運氣夠好,站在前面的第一個人就是售票員,否則,硬幣通常要在N個人手上傳遞,才能最終到達售票員的手里
中學時代的期末考試,如果平時不太老實,考試時就會被安排在第一個位置。遇到不會答的題目,就把題目編號寫在小紙條上往后傳遞,坐在后面的同學如果也不會答,他就會把這張小紙條繼續遞給他后面的人
從這兩個例子中,很容易找到職責鏈模式的最大優點:請求發送者只需要知道鏈中的第一個節點,從而弱化了發送者和一組接收者之間的強聯系。如果不使用職責鏈模式,那么在公交車上,就得先搞清楚誰是售票員,才能把硬幣遞給他。同樣,在期末考試中,也許就要先了解同學中有哪些可以解答這道題
假設負責一個售賣手機的電商網站,經過分別交納500元定金和200元定金的兩輪預定后(訂單已在此時生成),現在已經到了正式購買的階段。公司針對支付過定金的用戶有一定的優惠政策。在正式購買后,已經支付過500元定金的用戶會收到100元的商城優惠券,200元定金的用戶可以收到50元的優惠券,而之前沒有支付定金的用戶只能進入普通購買模式,也就是沒有優惠券,且在庫存有限的情況下不一定保證能買到
訂單頁面是php吐出的模板,在頁面加載之初,PHP會傳遞給頁面幾個字段
1、orderType:表示訂單類型(定金用戶或者普通購買用戶),code的值為1的時候是500元定金用戶,為2的時候是200元定金用戶,為3的時候是普通購買用戶
2、pay:表示用戶是否已經支付定金,值為true或者false。雖然用戶已經下過500元定金的訂單,但如果他一直沒有支付定金,現在只能降級進入普通購買模式
3、stock:表示當前用于普通購買的手機庫存數量,已經支付過500元或者200元定金的用戶不受此限制
下面把這個流程寫成代碼:
雖然得到了意料中的運行結果,但這遠遠算不上一段值得夸獎的代碼。order函數不僅巨大到難以閱讀,而且需要經常進行修改。雖然目前項目能正常運行,但接下來的維護工作無疑是個夢魘
職責鏈模式重構
現在我們職責鏈模式重構這段代碼,先把500元訂單、200元訂單以及普通購買分成3個函數。接下來把orderType、pay、stock這3個字段當作參數傳遞給500元訂單函數,如果該函數不符合處理條件,則把這個請求傳遞給后面的200元訂單函數,如果200元訂單函數依然不能處理該請求,則繼續傳遞請求給普通購買函數,代碼如下:
可以看到,執行結果和前面那個巨大的order函數完全一樣,但是代碼的結構已經清晰了很多,把一個大函數拆分了3個小函數,去掉了許多嵌套的條件分支語句
雖然已經把大函數拆分成了互不影響的3個小函數,但可以看到,請求在鏈條傳遞中的順序非常僵硬,傳遞請求的代碼被耦合在了業務函數之中
這依然是違反開放——封閉原則的,如果有天要增加300元預訂或者去掉200元預訂,意味著就必須改動這些業務函數內部。就像一根環環相扣打了死結的鏈條,如果要增加、拆除或者移動一個節點,就必須得先砸爛這根鏈條
靈活可拆分的職責鏈節點
下面采用一種更靈活的方式,來改進上面的職責鏈模式,目標是讓鏈中的各個節點可以靈活拆分和重組
首先需要改寫一下分別表示3種購買模式的節點函數,約定如果某個節點不能處理請求,則返回一個特定的字符串'nextSuccessor'來表示該請求需要繼續往后面傳遞:
接下來需要把函數包裝進職責鏈節點,定義一個構造函數Chain,在new Chain的時候傳遞的參數即為需要被包裝的函數,同時它還擁有一個實例屬性this.successor,表示在鏈中的下一個節點。此外Chain的prototype中還有兩個函數,它們的作用如下所示:
現在把3個訂單函數分別包裝成職責鏈的節點:
通過改進,可以自由靈活地增加、移除和修改鏈中的節點順序,假如某天網站運營人員又想出了支持300元定金購買,那就在該鏈中增加一個節點即可
異步的職責鏈
上面的職責鏈模式中,讓每個節點函數同步返回一個特定的值"nextSuccessor",來表示是否把請求傳遞給下一個節點。而在現實開發中,經常會遇到一些異步的問題,比如要在節點函數中發起一個ajax異步請求,異步請求返回的結果才能決定是否繼續在職責鏈中passRequet
這時候讓節點函數同步返回"nextSuccessor"已經沒有意義了,所以要給Chain類再增加一個原型方法Chain.prototype.next,表示手動傳遞請求給職責鏈中的下一個節點
下面是一個異步職責鏈的例子
現在得到了一個特殊的鏈條,請求在鏈中的節點里傳遞,但節點有權利決定什么時候把請求交給下一個節點。可以想象,異步的職責鏈加上命令模式,可以很方便地創建一個異步ajax隊列庫
優缺點
職責鏈模式的最大優點就是解耦了請求發送者和N個接收者之間的復雜關系,由于不知道鏈中的哪個節點可以處理發出的請求,所以只需把請求傳遞給第一個節點即可
在手機商城的例子中,本來要被迫維護一個充斥著條件分支語句的巨大的函數,在例子里的購買過程中只打印了一條log語句。其實在現實開發中,這里要做更多事情,比如根據訂單種類彈出不同的浮層提示、渲染不同的UI節點、組合不同的參數發送給不同的cgi等。用了職責鏈模式之后,每種訂單都有各自的處理函數而互不影響
其次,使用了職責鏈模式之后,鏈中的節點對象可以靈活地拆分重組。增加或者刪除一個節點,或者改變節點在鏈中的位置都是輕而易舉的事情
職責鏈模式還有一個優點,那就是可以手動指定起始節點,請求并不是非得從鏈中的第一個節點開始傳遞。比如在公交車的例子中,如果明確在前面的第一個人不是售票員,那當然可以越過他把公交卡遞給他前面的人,這樣可以減少請求在鏈中的傳遞次數,更快地找到合適的請求接受者。這在普通的條件分支語句下是做不到的,沒有辦法讓請求越過某一個if判斷
拿代碼來證明這一點,假設某一天網站中支付過定金的訂單已經全部結束購買流程,在接下來的時間里只需要處理普通購買訂單,所以可以直接把請求交給普通購買訂單節點:
orderNormal.passRequest(1,false,500); //普通購買,無優惠券
如果運用得當,職責鏈模式可以很好地幫助我們組織代碼,但這種模式也并非沒有弊端,首先不能保證某個請求一定會被鏈中的節點處理。比如在期末考試的例子中,小紙條上的題目也許沒有任何一個同學知道如何解答,此時的請求就得不到答復,而是徑直從鏈尾離開,或者拋出一個錯誤異常。在這種情況下,可以在鏈尾增加一個保底的接受者節點來處理這種即將離開鏈尾的請求
另外,職責鏈模式使得程序中多了一些節點對象,可能在某一次的請求傳遞過程中,大部分節點并沒有起到實質性的作用,它們的作用僅僅是讓請求傳遞下去,從性能方面考慮,要避免過長的職責鏈帶來的性能損耗
AOP
在之前的職責鏈實現中,利用了一個Chain類來把普通函數包裝成職責鏈的節點。其實利用JAVAscript的函數式特性,有一種更加方便的方法來創建職責鏈
下面改寫一下Function.prototype.after函數,使得第一個函數返回'nextSuccessor'時,將請求繼續傳遞給下一個函數,無論是返回字符串'nextSuccessor'或者false都只是一個約定,當然在這里也可以讓函數返回false表示傳遞請求,選擇'nextSuccessor'字符串是因為它看起來更能表達我們的目的,代碼如下
用AOP來實現職責鏈既簡單又巧妙,但這種把函數疊在一起的方式,同時也疊加了函數的作用域,如果鏈條太長的話,也會對性能有較大的影響
文件上傳
迭代器模式中,有一個獲取文件上傳對象的例子:當時創建了一個迭代器來迭代獲取合適的文件上傳對象,其實用職責鏈模式可以更簡單,完全不用創建這個多余的迭代器,完整代碼如下
在JavaScript開發中,職責鏈模式是最容易被忽視的模式之一。實際上只要運用得當,職責鏈模式可以很好地幫助我們管理代碼,降低發起請求的對象和處理請求的對象之間的耦合性。職責鏈中的節點數量和順序是可以自由變化的,可以在運行時決定鏈中包含哪些節點
無論是作用域鏈、原型鏈,還是DOM節點中的事件冒泡,都能從中找到職責鏈模式的影子。職責鏈模式還可以和組合模式結合在一起,用來連接部件和父部件,或是提高組合對象的效率
總結
優點
- 解耦了請求發送者和度個接收者之間的復雜關系,不需要知道鏈中哪個節點能處理你的請求,只需要把請求傳遞到第一個節點即可。
- 鏈中的節點對象可以靈活地拆分重組,增加或刪除一個節點,或者改變節點的位置都是很簡單的事情。
- 我們還可以手動指定節點的起始位置,并不是說非得要從其實節點開始傳遞的.
缺點
職責鏈模式中多了一點節點對象,可能在某一次請求過程中,大部分節點沒有起到實質性作用,他們的作用只是讓請求傳遞下去,從性能方面考慮,避免過長的職責鏈提高性能。