職責鏈模式(chain of responsibility)

衣舞晨風發表於2015-12-31

原文地址

一. 寫在前面的

這麼多的設計模式,我覺得職責鏈是我第一次看上去最簡單,可是回想起來卻又最複雜的一個模式。

因此,這個文章我醞釀了很久,一直也沒有膽量發出來,例子也是改了又改,可是仍然覺得不夠合理。所以希望各位多多指教。

二. 什麼是鏈

這裡寫圖片描述
文章伊始,先讓我們瞭解這個最基本的概念,什麼是鏈。

我給鏈下了這樣的定義:

  1. 鏈是一系列節點的集合。

  2. 鏈的各節點可靈活拆分再重組。

三. 何為職責鏈

職責鏈模式:使多個物件都有機會處理請求,從而避免請求的傳送者和接受者之間的耦合關係。將這個物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理他為止。

圖如下:
這裡寫圖片描述

四. 職責鏈模式應用之請假管理

請假這個事情,相信每個人都不陌生。

我們公司是個相對很寬鬆的公司。

在公司裡,如果你的請假時間小於0.5天,那麼只需要向專案經理打聲招呼就OK了。

如果超過了0.5天,但是還小於2天,那麼就要去找人事部處理,當然,這就要扣工資了。

如果超過了2天,你就需要去找總經理了,工資當然也玩完了。

那麼,對於我們來說,這個流程就是這樣的。
這裡寫圖片描述
也就是這樣一個過程,你需要和你的直接上級——專案經理去打交道,最終可能是專案經理給你回郵件,可能是人事部給你回郵件,也可能是總經理給你回郵件。內部的過程其實應該是個黑盒子,你並不知道內部的訊息是如何處理的。你需要找到的,只是你想要第一個交付的物件而已。
這裡寫圖片描述
那麼我們的程式碼應該是這樣的。

首先我們要寫一個請求的類。

class Request
{
    private int day;
    private string reason;
    public int Day
    {
        get { return day; }
        set { day = value; }
    }
    public string Reason
    {
        get { return reason; }
        set { reason = value; }
    }
    public Request(int day, string reason)
    {
        this.day = day;
        this.reason = reason;
    }
}

接下來看下請求相應者,他們有兩個核心方法,一個是相應操作,一個是選擇繼任者。

abstract class Boss
{
    private string name;
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
    private Boss successor;
    public Boss Successor
    {
        get { return successor; }
        set { successor = value; }
    }
    public Boss(string name)
    {
        this.name = name;
    }
    public abstract bool PassRequest(Request request);
}
class PM:Boss
{
    public PM(string name)
        : base(name)
    { }
    public override bool PassRequest(Request request)
    {
        int day = request.Day;
        string reason = request.Reason;
        if (day <= 0.5)
        {
            return true;
        }
        return Successor.PassRequest(request);
    }
}
class HR:Boss
{
    public HR(string name)
        : base(name)
    { }
    public override bool PassRequest(Request request)
    {
        int day = request.Day;
        string reason = request.Reason;
        if (day > 0.5&&day<=2)
        {
            return true;
        }
        return Successor.PassRequest(request);
    }
}
class Manager : Boss
{
    public Manager(string name)
        : base(name)
    { }
    public override bool PassRequest(Request request)
    {
        int day = request.Day;
        string reason = request.Reason;
        if (reason.Equals("正當理由"))
        {
            return true;
        }
        return false;
    }
}

那麼我們呼叫的時候就很簡單了!

static void Main(string[] args)
{
    Request request = new Request(3, "非正當理由");
    Boss pm = new PM("pm");
    Boss hr = new HR("hr");
    Boss manager = new Manager("manager");
    pm.Successor = hr;
    hr.Successor = manager;
    bool pass = pm.PassRequest(request);
    Console.Write(pass);
}

五. 靈活在哪?

讓我們來看下職責鏈究竟靈活在哪?

  1. 改變內部的傳遞規則。
    這裡寫圖片描述
    在內部,專案經理完全可以跳過人事部到那一關直接找到總經理。

每個人都可以去動態地指定他的繼任者。

2.可以從職責鏈任何一關開始。

如果專案經理不在,那麼完全可以寫這樣的程式碼:

static void Main(string[] args)
{
    Request request = new Request(3, "非正當理由");
    Boss pm = new PM("pm");
    Boss hr = new HR("hr");
    Boss manager = new Manager("manager");
    pm.Successor = hr;
    hr.Successor = manager;
    //bool pass = pm.PassRequest(request);
    bool pass = hr.PassRequest(request);
    Console.Write(pass);
}

3、我們來比較一下,用職責鏈和不用職責鏈的區別:
這裡寫圖片描述
這是不用職責鏈我們的結構,我們需要和公司中的每一個層級都發生耦合關係。

如果反映在程式碼上即使我們需要在一個類中去寫上很多醜陋的if….else語句。

如果用了職責鏈,相當於我們面對的是一個黑箱,我們只需要認識其中的一個部門,然後讓黑箱內部去負責傳遞就好了。

六. 職責鏈 != 連結串列

很多人都願意把職責鏈和連結串列混為一談,確實,從字面意思上理解,鏈,連結串列,很像。可是他們一樣麼?

他們區別在哪裡:

讓我們看一個連結串列的典型結構:
這裡寫圖片描述
讓我們來看一下連結串列的典型特徵:

  1. 連結串列是一個鏈狀結構,每個節點有一個next屬性去指向他的下一節點。

  2. 連結串列有一個Header節點,然後使用者每次必須通過頭節點,然後去遍歷尋找每一個節點。

  3. 連結串列遍歷操作的複雜度是O(n),但是插入和刪除指定節點的複雜度是常數級。

讓我們來著重看這第二點:

我們來想想在文章開始時我們畫出的那個鏈,一個鏈,我們可以從頭將他拿起,也可以從中間將他拿起:
這裡寫圖片描述
也就是說我們使用者可以去訪問節點中的任何一個節點作為開始節點,這就是連結串列與職責鏈不同的地方。

七. 職責鏈的擴充套件——樹狀鏈結構

職責鏈中,我們之前看到的都是一些單鏈結構,但是其實在很多情況下,每一個節點都對應著很多其他的部分。
這裡寫圖片描述
那麼這樣,我們的每一個節點都可以使用一個List來維護他節點的下一節點,甚至可以用組合模式來分別設計每一節點。

八. 由法律想到——職責鏈的兜底條款

仔細想想法律條文,尤其是刑法,經常可以看到這樣的條文:

  1. 如果***,則處以拘役處分。

  2. 如果***,則處以有期徒刑一年到十年。

  3. 如果***,則處以有期徒刑十年以上。

  4. 如果*,則**

  5. 如果以上條件皆不滿足,則*******

其實最後一條就叫做法律的兜底條款。這給了法官很大的自由裁量權,在一定程度上也降低了犯罪分子鑽法律空子的可能性。

在我們的職責鏈中,如果不存在這樣的兜底條款,那麼使用者如果不從首節點開始訪問,那麼就很可能出現異常的情況。於是我們應該為職責鏈設定一個預設的條款:
這裡寫圖片描述
這樣的話,任何一個處理無論如何訪問,都能得到一個正常的處理。

九. 職責鏈的缺點

讓我們繼續回到上面的例子,我們發現,其實當請假時間超過2天的時候,PM和HR其實沒有做任何的事情,而只是做了一個傳遞工作。

而傳遞工作之後,他們就成了垃圾物件。

也就是說,他們在實際的處理中,並沒有發揮任何的作用。

那麼當這個鏈結構比較長,比較複雜的話,會產生很多的記憶體垃圾物件。

這也就是職責鏈的最大缺點之所在。

十. 職責鏈的亂用

在和其他的人的討論中,我發現他們的觀點是:

只要一者傳一者,那麼就要用職責鏈。在我們的專案中,他們這樣去用:

abstract class DBHelper
{ 

}

interface IRequestHandler
{
    IDBHelper ReturnHelper(string dbName);
}
class RequestHandler:IRequestHandler
{
    private RequestHandler successor;
    public RequestHandler Successor
    {
        get { return successor; }
        set { successor = value; }
    }
    public abstract IDBHelper ReturnHelper(string dbName);
}

class SQLHelper : DBHelper
{ 

}
class OracleHelper : DBHelper
{ 

}
class DB2Helper : DBHelper
{ 

}
class SQL : RequestHandler
{
    public override IDBHelper ReturnHelper(string dbName)
    {
        if (dbName.Equals("SQL Server"))
        {
            return new SQLHelper();
        }
        return Successor.ReturnHelper(dbName);
    }
}
class Oracle : RequestHandler
{
    public override IDBHelper ReturnHelper(string dbName)
    {
        if (dbName.Equals("Oracle"))
        {
            return new OracleHelper();
        }
        return Successor.ReturnHelper(dbName);
    }
}
class DB2 : RequestHandler
{
    public override IDBHelper ReturnHelper(string dbName)
    {
        if (dbName.Equals("DB2"))
        {
            return new DB2Helper();
        }
        return new SQLHelper();
    }
}

這樣的話,每個類相當於只負責一個操作。

那麼我們如何改進呢?第一,我們可以用一個工廠來實現。另外,我們可以用表驅動的方式來解決問題。

十一. 表驅動改進職責鏈

表驅動(Table driven),其實就是指用查表的方式來獲取值。

那麼我們用標驅動法來改進上面的例子:

class HelperRequest
{
    private Dictionary<String, DBHelper> dic = new Dictionary<string, DBHelper>();
    public void Add(string name,DBHelper helper)
    {
        dic.Add(name, helper);
    }
    public DBHelper GetHelper(string name)
    {
        DBHelper helper;
        bool temp = dic.TryGetValue(name, out helper);
        if (temp)
        {
            return helper;
        }
        return null;
    }
}

我想一個沒有學過設計模式的人都會這樣寫的。一個學過設計模式很多年的人也會這樣寫的。

而怕的就是為了模式而模式,為了職責鏈而職責鏈了。

十二. 職責鏈在java script中的應用

我們想象這樣一種情況:
這裡寫圖片描述
我們都知道,在ASP.NET 的 Webform模型中頁面是以控制元件樹的形式去組織的。那麼我們用右鍵點選其中的一個頁面,那麼這個事件就會找離他最近的控制元件,如果不存在,那麼就去找他的父控制元件,如此遞迴下去,直到找到為止。

這其實就是一種職責鏈的體現!

十三. 深析職責鏈的使用

職責鏈模式不能亂用,否則非常容易變成因為模式而模式的反例。

下面是我歸納出來的一些關於職責鏈方面的使用規則,只是個人的意見,還希望大家指教。

1, 如果存在N對N,或者是一般的常規線性關係,那麼我們完全可以用表驅動來取代職責鏈。

2, 物件本身要經過什麼處理是通過每個鏈上元素通過執行態來決定的,決定的因素是取決於物件的屬性或者一些其他方面的策略。

3, 使用者無論是從哪一個節點作為他的請求頭節點,終端使用者都可以得到一個請求的反饋。

4, 應怪怪建議,補充同級的處理!職責鏈並非是嚴格的上下級的傳遞,其中也包括同級的傳遞,職責鏈一樣可以在同級之間做傳遞。

例如,繼續用我們上面請假的那個做例子,也許我們公司有兩個HR,事實上也是這樣的,我們把前臺“MM”也美稱為人力資源部:

static void Main(string[] args)
{
    Request request = new Request(3, "非正當理由");
    Boss pm = new PM("pm");
    Boss hr1 = new HR("Real HR");
    Boss hr2 = new HR("QiantaiMM");
    Boss manager = new Manager("manager");
    pm.Successor = hr1;
    hr1.Successor = hr2;
    hr2.Successor = manager;
    bool pass = pm.PassRequest(request);
    Console.Write(pass);
}

其實這樣也未嘗不可。有人也許會說,那麼這樣的同樣一個類的兩個物件又有什麼意義呢?

那麼我們不妨去試著這樣改造這個HR的類。

enum HRType
{
    RealHR,
    Qiantai
}
class HR:Boss
{
    private HRType type;
    public HR(string name,HRType type)
        : base(name)
    {
        this.type = type;
    }
    public override bool PassRequest(Request request)
    {
        int day = request.Day;
        if (day>=0.5&&day<2)
        {
            switch (type)
            { 
                case HRType.RealHR:
                    //扣工資
                    return true;
                    break;
                case HRType.Qiantai:
                    //不扣工資
                    return true;
                    break;
            }
        }
        return Successor.PassRequest(request);
    }
}

這樣,因為前臺MM容易說話,很可能他就不去扣你的工資,如果你去先找的HR,那麼你這天的工資就報銷了。

同理,我們一樣可以讓他們的職責細化,比如說Real Hr負責0.5天到1天的,而Qiantai去負責1天到2天的,也未嘗不可。

總之,職責鏈並非是單一的上下級的傳遞,一樣可以實現同級的傳遞。

十四. 職責鏈總結

1、Chain of Responsibility 模式的應用場合在於“一個請求可能有多個接受者,但是最後真正的接受者只有一個”,只有這時候請求傳送者與接受者的耦合才有可能出現“變化脆弱”的症狀,職責鏈的目的就是將二者解耦,從而更好地應對變化。
2、應用了Chain of Responsibility 模式後,物件的職責分派將更具靈活性。我們可以在執行時動態新增/修改請求的處理職責。
3、如果請求傳遞到職責鏈的末尾仍得不到處理,應該有一個合理的預設機制。這也是每一個接受物件的責任,而不是發出請求的物件的責任。

相關文章