Chain of Responsibility Design Pattern with C# Examples
leveraging proven design patterns can significantly enhance the efficiency and maintainability of our code. One such pattern is the Chain of Responsibility, which provides an elegant solution for managing complex workflow scenarios. In this article, we will explore the intricacies of the Chain of Responsibility design pattern, discussing its core concepts, benefits, and practical applications with examples.
Understanding the Chain of Responsibility Design Pattern: The Chain of Responsibility pattern is a behavioral design pattern that allows a request to be passed along a chain of potential handlers until it finds the appropriate one to process it. Each handler in the chain has the autonomy to handle the request or delegate it to the next handler. This decoupled approach promotes flexibility, scalability, and maintainability in workflow management scenarios.
Like many other behavioral design patterns, the Chain of Responsibility relies on transforming particular behaviors into stand-alone objects called handlers.
Key Participants:
- Handler: Defines an interface for handling requests and potentially sets the next handler in the chain.
- ConcreteHandler: Implements the Handler interface and handles requests it is responsible for. It may also delegate the request to the next handler in the chain.
- Client: Initiates the request and starts it on the chain of handlers.
Real-World Examples:
Example 1: code example that demonstrates the Chain of Responsibility pattern in C# for handling customer support tickets with different levels of severity in a customer support system:
using System;
// Handler interface
public interface ISupportHandler
{
void SetNextHandler(ISupportHandler handler);
void HandleTicket(Ticket ticket);
}
// Abstract base class for concrete handlers
public abstract class SupportHandlerBase : ISupportHandler
{
private ISupportHandler _nextHandler;
public void SetNextHandler(ISupportHandler handler)
{
_nextHandler = handler;
}
public virtual void HandleTicket(Ticket ticket)
{
// If this handler can handle the ticket, do the handling
if (CanHandleTicket(ticket))
{
Handle(ticket);
}
// If there is a next handler, pass the ticket to it
else if (_nextHandler != null)
{
_nextHandler.HandleTicket(ticket);
}
// No handler in the chain can handle the ticket
else
{
Console.WriteLine("Ticket cannot be handled.");
}
}
protected abstract bool CanHandleTicket(Ticket ticket);
protected abstract void Handle(Ticket ticket);
}
// Concrete handler: Level 1 Support
public class Level1SupportHandler : SupportHandlerBase
{
protected override bool CanHandleTicket(Ticket ticket)
{
return ticket.Severity == Severity.Low;
}
protected override void Handle(Ticket ticket)
{
Console.WriteLine("Level 1 Support handles the ticket.");
// Handle the ticket at Level 1 Support
}
}
// Concrete handler: Level 2 Support
public class Level2SupportHandler : SupportHandlerBase
{
protected override bool CanHandleTicket(Ticket ticket)
{
return ticket.Severity == Severity.Medium;
}
protected override void Handle(Ticket ticket)
{
Console.WriteLine("Level 2 Support handles the ticket.");
// Handle the ticket at Level 2 Support
}
}
// Concrete handler: Level 3 Support
public class Level3SupportHandler : SupportHandlerBase
{
protected override bool CanHandleTicket(Ticket ticket)
{
return ticket.Severity == Severity.High;
}
protected override void Handle(Ticket ticket)
{
Console.WriteLine("Level 3 Support handles the ticket.");
// Handle the ticket at Level 3 Support
}
}
// Ticket class
public class Ticket
{
public Severity Severity { get; set; }
// Other ticket properties
}
// Enum representing severity levels of tickets
public enum Severity
{
Low,
Medium,
High
}
// Usage example
public class Program
{
public static void Main()
{
// Create the support handlers
var level3SupportHandler = new Level3SupportHandler();
var level2SupportHandler = new Level2SupportHandler();
var level1SupportHandler = new Level1SupportHandler();
// Set the chain of responsibility
level1SupportHandler.SetNextHandler(level2SupportHandler);
level2SupportHandler.SetNextHandler(level3SupportHandler);
// Create tickets
var ticket1 = new Ticket { Severity = Severity.Low };
var ticket2 = new Ticket { Severity = Severity.Medium };
var ticket3 = new Ticket { Severity = Severity.High };
// Process the tickets
level1SupportHandler.HandleTicket(ticket1);
level1SupportHandler.HandleTicket(ticket2);
level1SupportHandler.HandleTicket(ticket3);
}
}
In this code example, we have the following components:
- The
ISupportHandler
interface defines the contract for support handlers, specifying theSetNextHandler
method to set the next handler in the chain and theHandleTicket
method to handle a support ticket. - The
SupportHandlerBase
abstract class provides a base implementation for concrete handlers. It holds the reference to the next handler in the chain and provides the default implementation of theHandleTicket
method. Concrete handlers extend this base class and implement theCanHandleTicket
andHandle
methods based on their specific criteria. - The concrete handler classes (
Level1SupportHandler
,Level2SupportHandler
, andLevel3SupportHandler
) inherit from theSupportHandlerBase
class. They override theCanHandleTicket
method to define their criteria for handling a ticket and implement theHandle
method to perform the actual ticket handling. - The
Ticket
class represents a customer support ticket and includes aSeverity
property to indicate the severity level of the ticket. - The
Severity
enum represents the severity levels of tickets. - In the
Main
method, we create instances of the support handler classes and set the chain of responsibility by calling theSetNextHandler
method. We also create three tickets with different severity levels and pass them to the first handler in the chain using theHandleTicket
method.
When you run the program, it will output the respective support handler that handles each ticket based on its severity level.
Code output:
Example 2:example where multiple handlers process the request before passing it to the next handler in the chain.
using System;
// Handler interface
public interface IRequestHandler
{
void SetNextHandler(IRequestHandler handler);
void HandleRequest(Request request);
}
// Abstract base class for concrete handlers
public abstract class RequestHandlerBase : IRequestHandler
{
private IRequestHandler _nextHandler;
public void SetNextHandler(IRequestHandler handler)
{
_nextHandler = handler;
}
public virtual void HandleRequest(Request request)
{
ProcessRequest(request);
if (_nextHandler != null)
{
_nextHandler.HandleRequest(request);
}
}
protected abstract void ProcessRequest(Request request);
}
// Concrete handler: Authentication Handler
public class AuthenticationHandler : RequestHandlerBase
{
protected override void ProcessRequest(Request request)
{
Console.WriteLine("Authentication handler processing request");
// Perform authentication logic
}
}
// Concrete handler: Authorization Handler
public class AuthorizationHandler : RequestHandlerBase
{
protected override void ProcessRequest(Request request)
{
Console.WriteLine("Authorization handler processing request");
// Perform authorization logic
}
}
// Concrete handler: Validation Handler
public class ValidationHandler : RequestHandlerBase
{
protected override void ProcessRequest(Request request)
{
Console.WriteLine("Validation handler processing request");
// Perform validation logic
}
}
// Request class
public class Request
{
public string Content { get; set; }
// Other request properties
}
// Usage example
public class Program
{
public static void Main()
{
// Create the request handlers
var authenticationHandler = new AuthenticationHandler();
var authorizationHandler = new AuthorizationHandler();
var validationHandler = new ValidationHandler();
// Set the chain of responsibility
authenticationHandler.SetNextHandler(authorizationHandler);
authorizationHandler.SetNextHandler(validationHandler);
// Create a request
var request = new Request { Content = "Sample request" };
// Process the request
authenticationHandler.HandleRequest(request);
}
}
In this example, we have the following components:
- The
IRequestHandler
interface defines the contract for request handlers, specifying theSetNextHandler
method to set the next handler in the chain and theHandleRequest
method to handle a request. - The
RequestHandlerBase
abstract class provides a base implementation for concrete handlers. It holds the reference to the next handler in the chain and provides the default implementation of theHandleRequest
method. Concrete handlers extend this base class and implement theProcessRequest
method to perform the specific processing logic. - The concrete handler classes (
AuthenticationHandler
,AuthorizationHandler
, andValidationHandler
) inherit from theRequestHandlerBase
class. They override theProcessRequest
method to perform their respective processing logic. - The
Request
class represents a request and includes properties such asContent
. - In the
Main
method, we create instances of the request handler classes and set the chain of responsibility by calling theSetNextHandler
method. We also create a request to be processed and pass it to the first handler in the chain using theHandleRequest
method.
When you run the program, each handler in the chain will process the request in the order specified. The output will show the handler name as each handler processes the request.
Example 3: Order Processing Workflow: To illustrate the practicality of the Chain of Responsibility pattern, let’s consider a real-world scenario: an order processing workflow. In this scenario, an order goes through various stages, including validation, inventory check, payment verification, and shipping confirmation. Each stage is represented by a handler, responsible for processing the order at that particular step.
Implementation: To implement the Chain of Responsibility pattern for our order processing workflow, we create concrete handler classes: ValidationHandler, InventoryCheckHandler, PaymentVerificationHandler, and ShippingConfirmationHandler. Each handler evaluates the order and either handles it or passes it to the next handler in the chain.
using System;
// Handler interface
public interface IOrderHandler
{
void SetNextHandler(IOrderHandler handler);
void ProcessOrder(Order order);
}
// Abstract base class for concrete handlers
public abstract class OrderHandlerBase : IOrderHandler
{
private IOrderHandler _nextHandler;
public void SetNextHandler(IOrderHandler handler)
{
_nextHandler = handler;
}
public virtual void ProcessOrder(Order order)
{
// If this handler can process the order, do the processing
if (CanProcessOrder(order))
{
Process(order);
}
// If there is a next handler, pass the order to it
else if (_nextHandler != null)
{
_nextHandler.ProcessOrder(order);
}
// No handler in the chain can process the order
else
{
Console.WriteLine("Order cannot be processed.");
}
}
protected abstract bool CanProcessOrder(Order order);
protected abstract void Process(Order order);
}
// Concrete handler: Validation Handler
public class ValidationHandler : OrderHandlerBase
{
protected override bool CanProcessOrder(Order order)
{
// Perform validation checks based on business rules
return true; // Placeholder condition, actual validation logic would go here
}
protected override void Process(Order order)
{
Console.WriteLine("Validation completed for order: " + order.OrderId);
// Perform order validation logic
}
}
// Concrete handler: Inventory Check Handler
public class InventoryCheckHandler : OrderHandlerBase
{
protected override bool CanProcessOrder(Order order)
{
// Check inventory availability based on business rules
return true; // Placeholder condition, actual inventory check logic would go here
}
protected override void Process(Order order)
{
Console.WriteLine("Inventory check completed for order: " + order.OrderId);
// Perform inventory check logic
}
}
// Concrete handler: Payment Verification Handler
public class PaymentVerificationHandler : OrderHandlerBase
{
protected override bool CanProcessOrder(Order order)
{
// Verify payment details based on business rules
return true; // Placeholder condition, actual payment verification logic would go here
}
protected override void Process(Order order)
{
Console.WriteLine("Payment verification completed for order: " + order.OrderId);
// Perform payment verification logic
}
}
// Concrete handler: Shipping Confirmation Handler
public class ShippingConfirmationHandler : OrderHandlerBase
{
protected override bool CanProcessOrder(Order order)
{
// Confirm shipping details based on business rules
return true; // Placeholder condition, actual shipping confirmation logic would go here
}
protected override void Process(Order order)
{
Console.WriteLine("Shipping confirmation completed for order: " + order.OrderId);
// Perform shipping confirmation logic
}
}
// Order class
public class Order
{
public string OrderId { get; set; }
public decimal TotalAmount { get; set; }
// Additional order properties
}
// Usage example
public class Program
{
public static void Main()
{
// Create the order handlers
var validationHandler = new ValidationHandler();
var inventoryCheckHandler = new InventoryCheckHandler();
var paymentVerificationHandler = new PaymentVerificationHandler();
var shippingConfirmationHandler = new ShippingConfirmationHandler();
// Set the chain of responsibility
validationHandler.SetNextHandler(inventoryCheckHandler);
inventoryCheckHandler.SetNextHandler(paymentVerificationHandler);
paymentVerificationHandler.SetNextHandler(shippingConfirmationHandler);
// Create an order
var order = new Order { OrderId = "12345", TotalAmount = 100.0M };
// Process the order
validationHandler.ProcessOrder(order);
}
}
In this example, we have the following components:
- The
IOrderHandler
interface defines the contract for order handlers, specifying theSetNextHandler
method to set the next handler in the chain and theProcessOrder
method to process an order. - The
OrderHandlerBase
abstract class provides a base implementation for concrete handlers. It holds the reference to the next handler in the chain and provides the default implementation of theProcessOrder
method. Concrete handlers extend this base class and implement theCanProcessOrder
andProcess
methods based on their specific criteria. - The concrete handler classes (
ValidationHandler
,InventoryCheckHandler
,PaymentVerificationHandler
, andShippingConfirmationHandler
) inherit from theOrderHandlerBase
class. They override theCanProcessOrder
method to define their criteria for processing an order and implement theProcess
method to perform the actual order processing. - The
Order
class represents an order and includes properties such asOrderId
andTotalAmount
. - In the
Main
method, we create instances of the order handler classes and set the chain of responsibility by calling theSetNextHandler
method. We also create an order to be processed and pass it to the first handler in the chain using theProcessOrder
method.
When you run the program, it will output the respective order handler that processes each order based on the specific criteria defined in each handler.
This code example demonstrates how the Chain of Responsibility pattern can be used to create a streamlined order processing workflow with multiple stages. Each handler in the chain has the opportunity to process the order or pass it to the next handler, allowing for a modular and flexible workflow management system.
Benefits of Using the Chain of Responsibility Pattern: The Chain of Responsibility pattern provides several benefits when handling complex workflows:
- Flexibility and Scalability: The pattern allows for dynamic reconfiguration of the workflow chain, enabling easy addition, removal, or modification of handlers without impacting the core logic. This flexibility facilitates seamless workflow management and future expansion.
- Maintainable and Modular Code: Each handler focuses on a specific task, ensuring that the code remains concise, modular, and easy to maintain. The separation of responsibilities simplifies debugging, testing, and updates.
- Enhanced Extensibility: The Chain of Responsibility pattern promotes code reuse, as handlers can be easily modified or replaced to accommodate evolving requirements. New handlers can be added to address additional stages or requirements without modifying existing code.
Use Cases of the Chain of Responsibility Pattern: The Chain of Responsibility pattern finds practical applications in various domains:
- Workflow and Approval Processes: Any scenario involving multi-step workflows or approval processes can benefit from the Chain of Responsibility pattern. It enables seamless transitions between steps and facilitates easy handling of exceptions or rejections at each stage.
- Event Handling: Systems that process events, such as UI frameworks or event-driven architectures, can utilize the Chain of Responsibility pattern to propagate events through a series of handlers, each responsible for a specific type or aspect of the event.
- Error Handling and Logging: The pattern can be employed to handle errors or logging scenarios where different handlers process errors or logs based on their severity or type.
Conclusion: The Chain of Responsibility design pattern offers an elegant and efficient solution for managing complex workflows in software systems. By passing requests through a chain of handlers, we achieve flexibility, maintainability, and extensibility. In our order processing workflow example, the Chain of Responsibility pattern streamlined the process, allowing each handler to focus on its unique responsibility.
In this article, we explored the core concepts of the Chain of Responsibility pattern in C#. By understanding its key participants, benefits, and real-world applications, you can apply this pattern to build robust and scalable software solutions.
References:
- Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)
- https://refactoring.guru/design-patterns/chain-of-responsibility