練習題 - 設計模式 - 責任鍊模式 (職責鍊模式) Chain of Responsibility Pattern

工作專案裡並不會去刻意地去使用設計模式,不過在很多時候為了想要有更好的實作,又或者是覺得當下的程式一定會有更好的解法,就會去找目前情境所適合使用的設計模式來改寫。

有的時後會看到誤用了不適合的設計模式所實作的程式,明明應該是用行為類的設計模式來做,卻用了結構類的設計模式來做,雖然最後的執行結果是有符合預期,但怎麼看就怎麼怪,所以設計模式還是要好好地認識認識,於是這次就來練習練習責任鍊模式。

責任鍊模式(Chain of Responsibility Pattern)

責任鍊模式(Chain of Responsibility Pattern)是一種行為設計模式,它允許你將請求的處理責任逐步傳遞給一系列的處理者(Handler),直到某個處理者處理該請求。這樣的設計允許請求的發送者不必知道最終處理者是誰,達到解耦的效果。

 

Chain of Responsibility design pattern
https://refactoring.guru/design-patterns/chain-of-responsibility

相關連結

 

未使用責任鍊模式的程式碼

以下的這段程式碼是個概念性的實作,而實際上的工作專案裡所實作的會更為複雜。

可以在 GetDataAsync 方法裡有做了六種情境的判斷,而這些的判斷都是一關接著一關,一個關卡過了之後才會接著做下一個關卡,如果其中一個關卡不符合的話就不會往下做而退回。

using Microsoft.AspNetCore.Mvc;
using WebApplication1.Services;

namespace WebApplication1.Controllers;

/// <summary>
/// class ExampleController
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ExampleController : ControllerBase
{
    private readonly ITokenService _tokenService;
    
    private readonly IUserService _userService;
    
    private readonly ITransactionService _transactionService;
    
    private readonly IExternalService _externalService;
    
    /// <summary>
    /// Initializes a new instance of the <see cref="ExampleController"/> class
    /// </summary>
    /// <param name="tokenService">The tokenService</param>
    /// <param name="userService">The userService</param>
    /// <param name="transactionService">The transactionService</param>
    /// <param name="externalService">The externalService</param>
    public ExampleController(ITokenService tokenService, 
                             IUserService userService, 
                             ITransactionService transactionService, 
                             IExternalService externalService)
    {
        this._tokenService = tokenService;
        this._userService = userService;
        this._transactionService = transactionService;
        this._externalService = externalService;
    }

    [HttpGet("data")]
    public async Task<IActionResult> GetDataAsync([FromHeader] string token)
    {
        // 1. 檢查 Header 是否輸入 Token
        if (string.IsNullOrEmpty(token))
        {
            return this.Unauthorized("Token is missing.");
        }

        // 2. 檢查 Token 是否有效
        var tokenIsValid = await this._tokenService.IsValidAsync(token);
        if (!tokenIsValid)
        {
            return this.Unauthorized("Token is invalid.");
        }

        // 3. 檢查 Token 是否過期
        var tokenExpired = await this._tokenService.IsExpiredAsync(token);
        if (tokenExpired)
        {
            return this.Unauthorized("Token has expired.");
        }

        // 4. 檢查用戶是否具有 "Admin" 角色
        var user = await this._userService.GetUserFromTokenAsync(token);
        if (user is null || !user.Roles.Contains("Admin"))
        {
            return this.Forbid("User does not have the required role.");
        }

        // 5. 檢查交易日額度限制
        var transactionLimitExceeded = await this._transactionService.IsTransactionLimitExceededAsync(token);
        if (transactionLimitExceeded)
        {
            return this.BadRequest("Transaction limit exceeded.");
        }

        // 6. 外部服務檢查
        var isValid = await this._externalService.ValidateRequestAsync(token);
        if (!isValid)
        {
            return this.BadRequest("External validation failed.");
        }

        // 所有驗證都通過,處理成功的邏輯
        return this.Ok("Success: All validations passed.");
    }
}

類似這樣的程式對於各位來說應該屢見不鮮,甚至是自己也常常會這樣實作。當這些判斷的邏輯隨著需求而增加或是要調整順序的時候,就如同對程式碼動大手術一般,要改動的地方會相當多。

另一方面則是有些人看到這樣的程式碼就會心煩意亂,覺得應該會有更好的解決方式來改善,讓這樣的 if 判斷不要隨著需求的改變而增加,並且用更好的方式去改善程式碼的可維護性與擴展性。

 

使用責任鍊模式(Chain of Responsibility Pattern)的修改

先來看看責任鍊模式的 UML 圖

https://www.programmergirl.com/chain-of-responsibility-pattern-java/

首先來定義一個 HandlerBase 的抽象類別,之後具體的實作 Handler 類別都會繼承它,另外還有一個 HandlerResult 類別做為判斷後的回傳型別

IHandler 介面定義兩個方法,一個是設定下一個處理者的方法以及當前處理者的實際執行處理邏輯的方法

/// <summary>
/// interface IHandler
/// </summary>
public interface IHandler
{
    /// <summary>
    /// SetNext
    /// </summary>
    /// <param name="nextHandler">The next handler</param>
    /// <returns>The handler</returns>
    IHandler SetNext(IHandler nextHandler);
    
    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    Task<HandlerResult> HandleAsync(string token);
}

HandlerBase  是一個抽象方法,繼承實作 IHandler,要注意的是 HandlerAsync  是一個 virtual 修飾的方法,之後繼承實作的具體處理者類別就需要去 override 這個方法,在這個方法裡去處理自己的業務邏輯。

/// <summary>
/// abstract class HandlerBase
/// </summary>
public abstract class HandlerBase : IHandler
{
    private IHandler _nextHandler;
    
    /// <summary>
    /// SetNext
    /// </summary>
    /// <param name="nextHandler">The next handler</param>
    /// <returns>The handler</returns>
    public IHandler SetNext(IHandler nextHandler)
    {
        this._nextHandler = nextHandler;
        return nextHandler;
    }

    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public virtual async Task<HandlerResult> HandleAsync(string token)
    {
        if (this._nextHandler is not null)
        {
            return await this._nextHandler.HandleAsync(token);
        }

        // 如果所有處理成功,回傳成功狀態
        return new HandlerResult(true);
    }
}

HandlerResult 類別是 HandlerAsync 方法所回傳的類別,裡面有兩個屬性,分別是 IsSuccessful 和 ActionResult,前者用來表示當前處理者執行 HandlerAsync 是否成功處理,後者則是當處理失敗後,要回傳的具體 IActionResult 結果

/// <summary>
/// class HandlerResult
/// </summary>
public class HandlerResult
{
    /// <summary>
    /// Is Successful
    /// </summary>
    public bool IsSuccessful { get; }

    /// <summary>
    /// ActionResult
    /// </summary>
    public IActionResult ActionResult { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="HandlerResult"/> class
    /// </summary>
    /// <param name="isSuccessful">The is successful</param>
    /// <param name="actionResult">The action result</param>
    public HandlerResult(bool isSuccessful, IActionResult actionResult = null)
    {
        this.IsSuccessful = isSuccessful;
        this.ActionResult = actionResult;
    }
}

再來是各個判斷處理的 Handler 實作類別

/// <summary>
/// class TokenExistHandler
/// </summary>
/// <remarks>
/// Token 檢查處理者
/// </remarks>
public class TokenExistHandler : HandlerBase
{
    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public override async Task<HandlerResult> HandleAsync(string token)
    {
        if (string.IsNullOrEmpty(token))
        {
            return new HandlerResult(
                isSuccessful: false,
                actionResult: new UnauthorizedObjectResult("Token is missing."));
        }
        
        return await base.HandleAsync(token);
    }
}
/// <summary>
/// class TokenValidationHandler
/// </summary>
/// <remarks>
/// Token 有效性檢查處理者
/// </remarks>
public class TokenValidationHandler : HandlerBase
{
    private readonly ITokenService _tokenService;

    /// <summary>
    /// Initializes a new instance of the <see cref="TokenValidationHandler"/> class
    /// </summary>
    /// <param name="tokenService">The token service</param>
    public TokenValidationHandler(ITokenService tokenService)
    {
        this._tokenService = tokenService;
    }

    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public override async Task<HandlerResult> HandleAsync(string token)
    {
        var tokenIsValid = await this._tokenService.IsValidAsync(token);
        
        if (!tokenIsValid)
        {
            return new HandlerResult(
                isSuccessful: false,
                actionResult: new UnauthorizedObjectResult("Token is invalid."));
        }
        
        return await base.HandleAsync(token);
    }
}
/// <summary>
/// class TokenExpirationHandler
/// </summary>
/// <remarks>
/// Token 過期檢查處理者
/// </remarks>
public class TokenExpirationHandler : HandlerBase
{
    private readonly ITokenService _tokenService;

    /// <summary>
    /// Initializes a new instance of the <see cref="TokenExpirationHandler"/> class
    /// </summary>
    /// <param name="tokenService">The token service</param>
    public TokenExpirationHandler(ITokenService tokenService)
    {
        this._tokenService = tokenService;
    }

    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public override async Task<HandlerResult> HandleAsync(string token)
    {
        var tokenExpired = await this._tokenService.IsExpiredAsync(token);
        
        if (tokenExpired)
        {
            return new HandlerResult(
                isSuccessful: false,
                actionResult: new UnauthorizedObjectResult("Token has expired."));
        }
        
        return await base.HandleAsync(token);
    }
}
/// <summary>
/// class RoleCheckHandler
/// </summary>
/// <remarks>
/// 角色檢查處理者
/// </remarks>
public class RoleCheckHandler : HandlerBase
{
    private readonly IUserService _userService;

    /// <summary>
    /// Initializes a new instance of the <see cref="RoleCheckHandler"/> class
    /// </summary>
    /// <param name="userService">The user service</param>
    public RoleCheckHandler(IUserService userService)
    {
        this._userService = userService;
    }

    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public override async Task<HandlerResult> HandleAsync(string token)
    {
        var user = await this._userService.GetUserFromTokenAsync(token);
        if (user is null || !user.Roles.Contains("Admin"))
        {
            return new HandlerResult(
                isSuccessful: false, 
                actionResult: new ForbidResult());
        }
        
        return await base.HandleAsync(token);
    }
}
/// <summary>
/// class TransactionLimitHandler
/// </summary>
/// <remarks>
/// 交易限制檢查處理者
/// </remarks>
public class TransactionLimitHandler : HandlerBase
{
    private readonly ITransactionService _transactionService;

    /// <summary>
    /// Initializes a new instance of the <see cref="TransactionLimitHandler"/> class
    /// </summary>
    /// <param name="transactionService">The transaction service</param>
    public TransactionLimitHandler(ITransactionService transactionService)
    {
        this._transactionService = transactionService;
    }

    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public override async Task<HandlerResult> HandleAsync(string token)
    {
        var transactionLimitExceeded = await this._transactionService.IsTransactionLimitExceededAsync(token);
        if (transactionLimitExceeded)
        {
            return new HandlerResult(
                isSuccessful: false,
                actionResult: new BadRequestObjectResult("Transaction limit exceeded."));
        }
        
        return await base.HandleAsync(token);
    }
}
/// <summary>
/// class ExternalServiceHandler
/// </summary>
/// <remarks>
/// 外部服務調用處理者
/// </remarks>
public class ExternalServiceHandler : HandlerBase
{
    private readonly IExternalService _externalService;

    /// <summary>
    /// Initializes a new instance of the <see cref="ExternalServiceHandler"/> class
    /// </summary>
    /// <param name="externalService">The external service</param>
    public ExternalServiceHandler(IExternalService externalService)
    {
        this._externalService = externalService;
    }

    /// <summary>
    /// Handles the token
    /// </summary>
    /// <param name="token">The token</param>
    /// <returns>A task containing the action result</returns>
    public override async Task<HandlerResult> HandleAsync(string token)
    {
        var isValid = await this._externalService.ValidateRequestAsync(token);
        if (!isValid)
        {
            return new HandlerResult(
                isSuccessful: false,
                actionResult: new BadRequestObjectResult("External validation failed."));
        }
        
        return await base.HandleAsync(token);
    }
}

在 Program.cs 裡註冊責任鍊的處理者實作類別

 

使用責任鍊模式修改後的 Controller 類別與 GetDataAsync 方法

在建構式裡注入各個 Handler 實作類別並依據執行順序去組合責任鍊

using Microsoft.AspNetCore.Mvc;
using WebApplication1.Handlers;

namespace WebApplication1.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ExampleWithChainController : ControllerBase
{
    private readonly IHandler _handlerChain;

    public ExampleWithChainController(TokenExistHandler tokenExistHandler,
                                      TokenValidationHandler tokenValidationHandler,
                                      TokenExpirationHandler tokenExpirationHandler,
                                      RoleCheckHandler roleCheckHandler,
                                      TransactionLimitHandler transactionLimitHandler,
                                      ExternalServiceHandler externalServiceHandler)
    {
        // 組合處理者責任鍊
        tokenExistHandler.SetNext(tokenValidationHandler)
                         .SetNext(tokenExpirationHandler)
                         .SetNext(roleCheckHandler)
                         .SetNext(transactionLimitHandler)
                         .SetNext(externalServiceHandler);

        this._handlerChain = tokenExistHandler;
    }
    
    [HttpGet("data")]
    public async Task<IActionResult> GetDataAsync([FromHeader] string token)
    {
        // 開始責任鍊的處理
        var result = await this._handlerChain.HandleAsync(token);
        
        // 如果處理不成功,返回對應的錯誤結果
        if (!result.IsSuccessful)
        {
            return result.ActionResult;
        }
        
        // 所有處理成功,回傳最終成功結果
        return this.Ok("Success: All validations passed.");
    }
}

在 HandlerBase 的 HandleAsync 方法裡有做了一個判斷,就是當所有的處理者都處理成功而沒有回傳的具體 IActionResult 時,就會直接回傳 true 的處理結果

而 GetDataAsync 方法會去判斷責任練組合處理結果,回傳結果的 IsSuccessful 為 false 的話就會回傳處理者所指定的 IActionResult 結果;如果所有處理者都處理成功,最後就會由 Action 方法回傳 OkObjectResult 和「Success: All validations passed.」訊息。

 

修改前後的比較

左邊的 ExampleController 是沒有責任鍊模式的原始樣貌,而右邊的 ExampleWithChainController  則是使用了責任鍊模式

或許有人會覺得以這個例子的修改,使用責任鍊模式好像只有 Controller 與 Action 方法有簡化而已,然後還要額外建立了很多的類別,看到要建立一堆的類別就會覺得累…

嗯…  使用責任鍊模式的改寫是完全符合單一職責原則(SRP, Single Responsibility Principle)和開放封閉原則(OCP, Open/Closed Principle)

單一職責原則 (SRP) - SRP 強調每個類別應該只有一個變更的理由,也就是它應該只負責一件事情。

每個處理者類別的職責是單一的,只會對其中一種業務邏輯進行處理。

開放封閉原則 (OCP) - OCP 的原則是,對擴展開放,對修改封閉。這意味著你應該能夠擴展系統的功能,而不必修改現有的程式碼。

當有新的判斷需求時,不需要去更動現有的處理者,只需要增加並實作處理者類別,以及在責任鍊組合裡在適當的順序裡去插入新的處理者就好,不需要更動到其餘的程式碼。

對於實際的工作專案裡,一定還有比這裡的程式還要複雜百倍以上,至少我看過的就有很多很多,如果有好好地使用責任鍊模式去改寫及實作,我想程式碼的可維護性會好上許多。不過對於物件導向或是 SOLID 嗤之以鼻的人來說,什麼責任鍊模式、設計模式的好處也都是白費功夫。

 

以上