C# 責任鏈模式 (Chain of Responsibility) 詳細教學

責任鏈模式 (Chain of Responsibility) ,是一種行為設計模式,能夠讓指令沿著處理程序鏈傳遞請求,每個處理程序自行決定攔截並處理還是傳遞給下一個處理程序。主要使用在針對某一個任務,在面對不同情況時使用不同方式處理。
有沒有白話一點的說法?就是如果你的程式中有很大一包的 if else (或是 switch case),或許就適合使用責任鍊模式了。

舉個例子,假設現在有個需求,需要持續監聽 log 訊息,依照開頭的字串如 debug, info, error 等使用不同的處理方式
在一般的做法中可能會這樣寫:
    
if (input.StartsWith("debug"))
{
    // todo: debug 的處理方式
}
else if (input.StartsWith("info"))
{
    // todo: info 的處理方式
}
else if (input.StartsWith("error"))
{
    // todo: error 的處理方式
}
else
{
    // 不處理
}
    

若每種處理方式都很簡短,需要處理的方式也不多,那就沒有什麼問題。但是隨著要監視的任務越來越多、每個處理方式也越來越複雜,這個 if-else 的迴圈就會越來越大,越來越難以維護。

這個時候使用責任鏈模式就可以將各個 if 拆分成各自的處理程序,在各自的類別中判斷需要處理還是傳遞給下一個處理程序。

實作

建立一個抽象類別,讓所有處理程序來繼承。每個處理程序在建立時直接在建構子指定下一個處理程序,這樣此處理程序不處理時就能夠知道要交由哪個處理程序來處理,若放入 NULL 則代表這是最後一個處理程序。
    
/// <summary>
/// 處理程序介面
/// </summary>
public abstract class AbstractHandler
{
    /// <summary>
    /// 下一個處理程序
    /// </summary>
    protected readonly AbstractHandler? NextHandler;

    /// <summary>
    /// 指定下一個處理程序
    /// </summary>
    protected AbstractHandler(AbstractHandler? nextHandler)
    {
        NextHandler = nextHandler;
    }

    /// <summary>
    /// 處理請求或是讓下一個處理程序處理
    /// </summary>
    public abstract void Handle(string request);
}
    

debug 處理程序
    
/// <summary>
/// 處理字串為 debug 開頭的處理程序
/// </summary>
public class DebugHandler : AbstractHandler
{
    public DebugHandler(AbstractHandler? next) : base(next)
    {
    }

    public override void Handle(string request)
    {
        if (request.StartsWith("debug"))
        {
            // todo: 字串為 debug 開頭的處理方式
            Console.WriteLine("DebugHandler: " + request);
        }
        else
        {
            // 如果有下一個處理程序,則交由下一個處理程序執行
            NextHandler?.Handle(request);
        }
    }
}
    

info 處理程序
    
/// <summary>
/// 處理字串為 info 開頭的處理程序
/// </summary>
public class InfoHandler : AbstractHandler
{
    public InfoHandler(AbstractHandler? nextHandler) : base(nextHandler)
    {
    }

    public override void Handle(string request)
    {
        if (request.StartsWith("info"))
        {
            // todo: 字串為 info 開頭的處理方式
            Console.WriteLine("InfoHandler: " + request);
        }
        else
        {
            // 如果有下一個處理程序,則交由下一個處理程序執行
            NextHandler?.Handle(request);
        }
    }
}
    

使用方式:
    
AbstractHandler? handler = new DebugHandler(new InfoHandler(null));


// 手動輸入資料測試
handler?.Handle("debug data");
handler?.Handle("info data");
handler?.Handle("data");

/*
輸出結果:
DebugHandler: debug data
InfoHandler: info data
*/
    


未來要增加新的處理程序時只要建立新的繼承 AbstractHandler 的類別,並在 InfoHandler 後面增加即可(對擴充開放)。
例如增加 ErrorHandler:
    
AbstractHandler? handler = new DebugHandler(new InfoHandler(new ErrorHandler(null)));
    

除了上面這行以外不會動到其他處理程序類別中的程式碼(對修改封閉), 很好的遵守了開放封閉原則 (Open-Closed Principle, OCP)

不過其實上面的程式碼在 Handle 方法中有些地方重複了,就是「交由下一個處理程序執行」的部分
    
    public override void Handle(string request)
    {
        if (request.StartsWith("info"))
        {
            // todo: 字串為 info 開頭的處理方式
            Console.WriteLine("InfoHandler: " + request);
        }
        else
        {
            // 如果有下一個處理程序,則交由下一個處理程序執行
            NextHandler?.Handle(request);
        }
    }
    

一開始介紹時沒有直接抽出來是因為在還不了解責任鏈模式時直接跳過太多步驟可能會更不容易理解,現在我們可以使用另一個設計模式 —— 樣板方法(Template Method) ,將重複的部分抽取至抽象類別中(abstract) 。

重構

新的 AbstractHandler 抽象類別:
    
/// <summary>
/// 處理程序介面
/// </summary>
public abstract class AbstractHandler
{
    /// <summary>
    /// 下一個處理程序
    /// </summary>
    private readonly AbstractHandler? _nextHandler;

    /// <summary>
    /// 指定下一個處理程序
    /// </summary>
    protected AbstractHandler(AbstractHandler? nextHandler)
    {
        _nextHandler = nextHandler;
    }

    public void Handle(string request)
    {
        if (CanHandle(request))
        {
            DoHandle(request);
        }
        else
        {
            _nextHandler?.Handle(request);
        }
    }

    /// <summary>
    /// 檢查是否能處理
    /// </summary>
    protected abstract bool CanHandle(string request);

    /// <summary>
    /// 處理
    /// </summary>
    protected abstract void DoHandle(string request);
}

    

debug 處理程序
    
/// <summary>
/// 處理字串為 debug 開頭的處理程序
/// </summary>
public class DebugHandler : AbstractHandler
{
    public DebugHandler(AbstractHandler? nextHandler) : base(nextHandler)
    {
    }

    protected override bool CanHandle(string request)
    {
        return request.StartsWith("debug");
    }

    protected override void DoHandle(string request)
    {
        Console.WriteLine($"DebugHandler: {request}");
    }
}
    

info 處理程序
    
/// <summary>
/// 處理字串為 info 開頭的處理程序
/// </summary>
public class InfoHandler : AbstractHandler
{
    public InfoHandler(AbstractHandler? nextHandler) : base(nextHandler)
    {
    }

    protected override bool CanHandle(string request)
    {
        return request.StartsWith("info");
    }

    protected override void DoHandle(string request)
    {
        Console.WriteLine($"InfoHandler: {request}");
    }
}
    

這樣各個方法的職責又更明確,未來要維護專案的開發人員也能夠更加容易的了解程式碼,而不用在幾百行的 if-else 程式碼中苦苦掙扎。

參考資料:
Refactoring.guru - Chain of Responsibility
Refactoring.guru - Template Method

留言