Design Patterns
- Chapter 1: Introduction to Design Patterns
- Chapter 2: Creational Design Patterns
- Chapter 3: Singleton Pattern
- Chapter 4: Factory Method Pattern
- Chapter 5: Abstract Factory Pattern
- Chapter 6: Builder Pattern
- Chapter 7: Prototype Pattern
- Chapter 8: Structural Design Patterns
- Chapter 9: Adapter Pattern
- Chapter 10: Bridge Pattern
- Chapter 11: Composite Pattern
- Chapter 12: Decorator Pattern
- Chapter 13: Facade Pattern
- Chapter 14: Flyweight Pattern
- Chapter 15: Proxy Pattern
- Chapter 16: Behavioral Design Patterns
- Chapter 17: Chain of Responsibility Pattern
- Chapter 18: Command Pattern
- Chapter 19: Interpreter Pattern
- Chapter 20: Iterator Pattern
- Chapter 21: Mediator Pattern
- Chapter 22: Memento Pattern
- Chapter 23: Observer Pattern
- Chapter 24: State Pattern
- Chapter 25: Strategy Pattern
- Chapter 26: Template Method Pattern
- Chapter 27: Visitor Pattern
- Chapter 28: Design Patterns in Real-World Applications
- Chapter 29: Pros and Cons of Design Patterns
- Chapter 30: Best Practices for Using Design Patterns
Tutorials – Design Patterns
Chapter 17: Chain of Responsibility Pattern
The Chain of Responsibility Pattern is a behavioral design pattern that promotes the idea of passing a request along a chain of handlers. Each handler decides either to process the request or pass it to the next handler in the chain. This pattern decouples the sender of a request from its receiver, giving more than one object a chance to handle the request.
Understanding the Chain of Responsibility Pattern
In software design, there are scenarios where an object initiates a request, but the exact handler for that request is not known in advance. The Chain of Responsibility Pattern addresses this issue by creating a chain of handler objects. When a request is made, it is passed along this chain until a suitable handler is found to process the request.
Key Participants:
- Handler: The Handler is an interface or an abstract class that defines a method for handling requests. It also contains a reference to the next handler in the chain.
- ConcreteHandler: ConcreteHandler classes implement the Handler interface. They are responsible for handling specific types of requests. If they can’t handle the request, they pass it to the next handler in the chain.
- Client: The Client initiates the request and starts the chain. It doesn’t need to know the exact handlers in the chain; it only communicates with the first handler.
How It Works
The Chain of Responsibility Pattern works by forming a linked list of handler objects. When a request is made, it is passed to the first handler in the chain. This handler evaluates the request and decides whether to process it or pass it to the next handler. This process continues until a handler in the chain successfully processes the request or until the end of the chain is reached.
The key to the pattern is that each handler has a reference to the next handler in the chain. This reference is typically set during the configuration or setup phase. Handlers can be added, removed, or reordered without affecting the Client’s code.
Use Cases
The Chain of Responsibility Pattern is useful in various scenarios:
- Event Handling: In graphical user interfaces, events like mouse clicks or keyboard inputs can be processed by a chain of event handlers.
- Logging Systems: Log messages can be processed by a chain of loggers, each responsible for a different level of logging (e.g., info, warning, error).
- Middleware in Web Development: In web applications, middleware components can be organized as a chain to handle requests for tasks like authentication, authorization, and routing.
- Request Processing: In server-side applications, HTTP requests can pass through a chain of processors responsible for tasks like compression, encryption, or authentication.
- Validation Frameworks: Validations can be organized in a chain where each validator checks a specific aspect of the data.
Implementing the Chain of Responsibility Pattern
Let’s walk through the implementation of the Chain of Responsibility Pattern:
- Handler Interface or Abstract Class: Create an interface or an abstract class that defines a method for handling requests. This interface should also contain a reference to the next handler in the chain.
- Concrete Handlers: Implement concrete handler classes that extend the Handler interface or abstract class. Each concrete handler should provide its implementation of the request handling method and decide whether to pass the request to the next handler.
- Chaining Handlers: Set up the chain of responsibility by linking the handlers. Each handler should have a reference to the next handler in the chain.
- Client Code: The client code initiates the request and starts the chain by passing the request to the first handler in the chain. It doesn’t need to know the exact handlers in the chain.
Benefits of the Chain of Responsibility Pattern
The Chain of Responsibility Pattern offers several benefits:
- Loose Coupling: It decouples the sender of a request from its receiver, providing flexibility in the assignment of handlers.
- Multiple Handlers: It allows for multiple handlers to process a request, increasing the chances of finding a suitable handler.
- Dynamic Chain: Handlers can be added, removed, or reordered dynamically without changing the client’s code.
- Reusability: Handler classes can be reused in different chains or scenarios.
Drawbacks of the Chain of Responsibility Pattern
While the Chain of Responsibility Pattern provides flexibility, it also has some drawbacks:
- Uncertainty: There is no guarantee that a request will be handled. If the end of the chain is reached and no handler can process the request, it remains unhandled.
- Performance: If the chain is long or if there are many requests, it can introduce a performance overhead.
Example: Logging System
Let’s look at a simple example of the Chain of Responsibility Pattern in a logging system.
Suppose we have different types of loggers: InfoLogger, WarningLogger, and ErrorLogger. Each logger can handle log messages of a specific level. If a logger receives a log message of its level or higher, it processes the message; otherwise, it passes the message to the next logger in the chain.
// Handler Interface
public abstract class Logger
{
protected Logger nextLogger;
public void SetNextLogger(Logger nextLogger)
{
this.nextLogger = nextLogger;
}
public abstract void LogMessage(string message, LogLevel level);
}
// Concrete Handlers
public class InfoLogger : Logger
{
public override void LogMessage(string message, LogLevel level)
{
if (level <= LogLevel.INFO)
{
Console.WriteLine($"Info Logger: {message}");
}
else if (nextLogger != null)
{
nextLogger.LogMessage(message, level);
}
}
}
public class WarningLogger : Logger
{
public override void LogMessage(string message, LogLevel level)
{
if (level <= LogLevel.WARNING)
{
Console.WriteLine($"Warning Logger: {message}");
}
else if (nextLogger != null)
{
nextLogger.LogMessage(message, level);
}
}
}
public class ErrorLogger : Logger
{
public override void LogMessage(string message, LogLevel level)
{
if (level <= LogLevel.ERROR)
{
Console.WriteLine($"Error Logger: {message}");
}
else if (nextLogger != null)
{
nextLogger.LogMessage(message, level);
}
}
}
// LogLevel Enumeration
public enum LogLevel
{
INFO,
WARNING,
ERROR
}
// Client Code
class Program
{
static void Main()
{
// Create the chain of loggers
Logger infoLogger = new InfoLogger();
Logger warningLogger = new WarningLogger();
Logger errorLogger = new ErrorLogger();
infoLogger.SetNextLogger(warningLogger);
warningLogger.SetNextLogger(errorLogger);
// Simulate log messages
infoLogger.LogMessage("This is an informational message.", LogLevel.INFO);
infoLogger.LogMessage("This is a warning message.", LogLevel.WARNING);
infoLogger.LogMessage("This is an error message.", LogLevel.ERROR);
}
}
In this example, log messages are passed through a chain of loggers, each responsible for handling messages of a specific level or passing them to the next logger in the chain.
Conclusion
The Chain of Responsibility Pattern is a powerful and flexible way to handle requests in a decoupled and extensible manner. It is especially useful when you have a series of handlers that can be organized as a chain, and you want to dynamically assign responsibilities. By understanding the pattern’s structure and benefits, you can effectively apply it to various scenarios, improving the maintainability and flexibility of your software.