1. Tổng quan
"The Chain of Responsibility pattern establishes a chain within a system, so that a message can either be handled at the level where it is first received, or be directed to an object that can handle it." - Gang of Four
Chain of Responsibility là một design pattern thuộc nhóm Behavioral Pattern. Behavioral Pattern bao gồm những pattern quan tâm đến hành vi của đối tượng, sự giao tiếp giữa các đối tượng với nhau.
Ý tưởng chính của Chain of Responsibility pattern:
- Cho phép ta truyền request qua một chuỗi các hàm xử lý (các handlers).
- Khi nhận được request, mỗi handler quyết định xử lý request đó hay chuyển qua handler tiếp theo trong chuỗi.
2. Vấn đề
Tưởng tượng bạn đang phát triển một hệ thống đặt hàng online. Bạn muốn giới hạn truy cập đến hệ thống sao cho chỉ những users đã xác thực có thể đặt hàng. Ngoài ra, users có quyền admin có quyền truy cập đơn hàng của tất cả đơn hàng.
Sau một hồi tính toán, bạn nhận ra quy trình này cần được làm tuần tự. Ứng dụng sẽ xác minh user khi nó được request. Tuy nhiên, nếu danh tính user không đúng và xác thực thất bại, không có lý do gì để ta tiếp tục các bước kiểm tra tiếp theo.
Request phải đi qua một chuỗi kiểm tra trước khi hệ thống thật sự xử lý đơn hàng.
Sau vài tháng tiếp theo, bạn phát triển thêm một vài bước trước khi xử lý request khác:
- Nhận ra việc truyền trực tiếp data vào hệ thống là một cách không an toàn, bạn thêm một bước xử lý data của request.
- Một vụ việc xảy ra: trang web bị hacker tấn công bằng cách brute force password, bạn quyết định thêm một bước ngăn chặn các request thất bại từ cùng 1 địa chỉ IP.
- Sếp yêu cầu bạn tối ưu hệ thống bằng cách trả về cached result cho những request có nội dung giống nhau. Bạn thêm một lần check nữa xem đã có cached result cho request chưa.
Code của các công đoạn trở nên càng cồng kềnh và phức tạp khi bạn càng thêm nhiều chức năng. Thay đổi một bước check có thể ảnh hưởng các check khác. Tình huống xấu nhất là khi bạn cần resue một vài bước check cho component khác, bạn phải diplicate các dòng code.
Vấn đề yêu cầu bạn phải tổ chức code một cách hợp lý hơn.
3. Giải pháp
Như bao design pattern hành vi khác, Chain of Responsibility (CoR) biến từng hành vi cụ thể thành các object gọi là handler. Trong trường hợp này, mỗi công đoạn check nên được trích ra thành từng class riêng với 1 method để thực hiện việc check. Request được truyền vào method này dưới dạng một đối số.
CoR khuyên ta nên liên kết các handler này thành một chuỗi. Mỗi handler lưu trữ reference tới handler tiếp theo trong chuỗi (giống linked list). Request đi qua như một hàng hóa trên dây chuyền cho tới khi tất cả handler đều có cơ hội xử lý request đó.
Và điều tuyệt vời hơn là: mỗi handler có thể quyết định đưa request đi xa hơn hay dừng request tại thời điểm đó một cách hiệu quả.
CoR còn được dùng theo hướng tiếp cận khác, và cũng là hướng kinh điển hơn như trong định nghĩa, khi nhận được request, handler quyết định có xử lý nó hay không. Nếu nó có thể, nó không đưa request đi xa hơn nữa. Nên cách tiếp cận này rất thông dụng trong xử lý các events trong một stack các elements trên giao diện. Ví dụ, khi người dùng ấn một nút, sự kiện được truyền qua một chuỗi các UI element, bắt đầu từ button, đến container, panel, rồi cuối cùng là cửa sổ ứng dụng chính. Event được xử lý tại handler trong chuỗi có trách nhiệm xử lý nó. Chuỗi này cũng chính là một nhánh của object tree.
Lưu ý, tất cả handler class đều cần implement cùng interface.
4. Cấu trúc
- Một Handler Interface chung cho tất cả các handlers. Nó thường bao gồm một phương thức xử lý request và phương thức để xác định handler nối tiếp trong chuỗi.
- Bạn có thể thêm Base Handler để chứa boilerplate code dùng chung cho tất cả handler. Thông thường, BaseHandler định nghĩa 1 trường để lưu reference đến handler tiếp theo. Clients có thể xây dựng chuỗi xử lý bằng truyền handler vào constructor hoặc vào phương thức setNext của handler trước.
- Concrete Handler chứa đoạn code xử lý request. Khi nhận được request, mỗi handler quyết định có xử lý nó không, và đôi khi quyết định chuyển đến handler nào trong chuỗi.
- Client chỉ cần lập ra đường đi của chuỗi handler một lần, hoặc trong thời gian chạy, tùy vào logic ứng dụng. Lưu ý: Request có thể gửi đến bất kỳ handler nào của chuỗi, không cần phải là handler đầu tiên.
5. Code mẫu
Một ví dụ điển hình của Chain of Responsibility trong việc xử lý các sự kiện của Button trong một trang nhập liệu đơn giản. Thành phần trang bao gồm Button được gói bên trong Tooltip, Tooltip lại được gói bên trong Form. Các sự kiện render, hover và submit đều được xuất phát từ Button, nhưng class giải quyết sự kiện lần lượt là Button, Tooltip và Form.
Ngôn ngữ: C#
using System;
using System.Collections.Generic;
namespace DPCor
{
public interface IHandler
{
IHandler setNext(IHandler handler);
object handle(object request);
}
abstract class BaseHandler : IHandler
{
private IHandler _nextHandler;
public IHandler setNext(IHandler handler)
{
this._nextHandler = handler;
return handler;
}
public virtual object handle(object request)
{
if (this._nextHandler != null)
{
return this._nextHandler.handle(request);
}
else
{
return null;
}
}
class ButtonHandler : BaseHandler
{
public override object handle(object request)
{
if ((request as string) == "Render")
return $"Button: I'm rendered.";
else
return base.handle(request);
}
}
class TooltipHandler : BaseHandler
{
public override object handle(object request)
{
if ((request as string) == "Hover")
return $"Tooltip: You are hovering the button.";
else
return base.handle(request);
}
}
class FormHandler : BaseHandler
{
public override object handle(object request)
{
if ((request as string) == "Submit")
return $"Form: Your form was submitted.";
else
return base.handle(request);
}
}
class Program
{
public static void Main(string[] args)
{
var submitButton = new ButtonHandler();
var tooltip = new TooltipHandler();
var form = new FormHandler();
//Compose chain
submitButton.setNext(tooltip).setNext(form);
//Client code
Console.WriteLine(submitButton.handle("Render"));
Console.WriteLine(submitButton.handle("Hover"));
Console.WriteLine(submitButton.handle("Submit"));
}
}
}
}
Output:
Button: I'm rendered.
Tooltip: You are hovering the button.
Form: Your form was submitted.
6. Ứng dụng
- Sử dụng CoR khi ứng dụng cần xử lý nhiều loại request bằng nhiều cách khác nhau, nhưng tổ hợp những loại request là gì hoặc thứ tự xử lý như thế nào không được biết trước.
- Sử dụng CoR khi cần xử lý nhiều handlers theo một thứ tự nhất định.
- Sử dụng CoR khi tổ hợp các handlers và thứ tự của nó được thay đổi trong lúc chạy chương trình.
7. Đánh giá
7.1. Ưu điểm
- Bạn có thể điều khiển thứ tự của các bước xử lý request.
- Single Responsibility Principle. Bạn có thể phân tách giữa lớp kích hoạt operation và lớp thực hiện operation.
- Open/Closed Principle. Bạn có thể thêm handlers vào app mà không cần thay đổi code cũ của client.
7.2. Nhược điểm
- Một vài request có thể không được handle.
8. So sánh
- Chain of Responsibility, Command, Mediator và Observer đều đưa ra những cách khác nhau để liên kết giữa sender và receiver của request:
- Chain of Responsibility đưa request qua một chuỗi các receivers cho tới khi có một receiver xử lý.
- Command thiết lập các kết nối vô hướng giữa senders và receivers.
- Mediator loại bỏ những kết nối trực tiếp giữa senders và receivers, buộc các class liên lạc với nhau gián tiếp thông qua mediator object.
- Observer cho phép receivers subscribe và unsubscribe các request trong thời gian chạy.
- CoR có thể kết hợp với Composite. Khi đó, khi một component lá nhận được request, nó sẽ truyền request qua chuỗi các parent components đến root của object tree.
- Handlers của CoR có thể hiện thực bằng Commands. Khi đó, bạn có thể thực thi nhiều operations khác nhau cho một request. Ngoài ra, còn một cách tiếp cận khác: Chính request là Command object. Lúc này, bạn có thể thực thi cùng một operation trong một chuỗi các contexts khác nhau.
- CoR và Decorator có cấu trúc lớp khá giống nhau. Cả 2 cùng gọi đệ quy để truyền việc thực thi qua một chuỗi các objects. Tuy nhiên, CoR handlers hoàn toàn độc lập các operations với nhau, và có khả năng ngừng truyền request đi xa hơn bất cứ lúc nào. Decorators mở rộng hành vi của object đồng thời giữ nó nhất quán với interface và không có khả năng phá vỡ đường đi của request.
Nguồn tham khảo
- Alexander Shvets (refactoring.guru) - Dive Into Design Patterns.
- Erich Gamma, John Vlissides, Richard Helm, Ralph Johnson - Design Patterns: Elements of Reusable Object-Oriented Software.