Lesson 4
Introduction to the Chain of Responsibility Pattern
Introduction to the Chain of Responsibility Pattern

Welcome back! Building on our exploration of various behavioral patterns, we will delve into the Chain of Responsibility Pattern in this lesson. This pattern allows a request to pass through a chain of handlers, giving multiple objects a chance to process the request. It's an excellent way to achieve loose coupling in your code by enabling the request to be processed by different handlers independently.

What You'll Learn

In this lesson, you will learn how to implement the Chain of Responsibility Pattern in Go. This pattern allows you to handle requests flexibly and dynamically. Specifically, you will learn to:

  1. Create a Handler Interface: Define a common interface for all your handlers.
  2. Implement Concrete Handlers: Learn to create specific handlers that deal with different levels of requests.
  3. Set Up the Chain: Understand how to link these handlers together so that a request passes from one to another until it's handled.

Let's quickly go through an example to understand these concepts:

We start by defining a Request struct and a Handler interface. The Request struct contains the message and the level of the request, while the Handler interface defines the common methods that all handlers must implement:

Go
1type Request struct { 2 message string 3 level int 4} 5 6type Handler interface { 7 Handle(request Request) bool 8 SetNext(handler Handler) 9}

Next, we create a BaseHandler struct that implements the Handler interface. Notice, the BaseHandler contains a reference to the next handler in the chain by using the next field. The Handle method in the BaseHandler passes the request to the next handler if it exists:

Go
1// Base Handler 2type BaseHandler struct { 3 next Handler 4} 5 6func (h *BaseHandler) SetNext(handler Handler) { 7 h.next = handler 8} 9 10func (h *BaseHandler) Handle(request Request) bool { 11 if h.next != nil { 12 return h.next.Handle(request) 13 } 14 return false 15}

Now, we create specific handlers that extend the BaseHandler and implement the Handle method. In our example, we have FrontDeskHandler, ManagerHandler, and DirectorHandler. Each handler checks if it can handle the request based on the request level. If it can't, it passes the request to the next handler in the chain:

Go
1// Front Desk Handler 2type FrontDeskHandler struct { 3 BaseHandler 4} 5 6func (h *FrontDeskHandler) Handle(request Request) bool { 7 if request.level == 1 { 8 fmt.Println("Request handled by Front Desk") 9 return true 10 } 11 return h.BaseHandler.Handle(request) 12} 13 14// Manager Handler 15type ManagerHandler struct { 16 BaseHandler 17} 18 19func (h *ManagerHandler) Handle(request Request) bool { 20 if request.level == 2 { 21 fmt.Println("Request handled by Manager") 22 return true 23 } 24 return h.BaseHandler.Handle(request) 25} 26 27// Director Handler 28type DirectorHandler struct { 29 BaseHandler 30} 31 32func (h *DirectorHandler) Handle(request Request) bool { 33 if request.level == 3 { 34 fmt.Println("Request handled by Director") 35 return true 36 } 37 return h.BaseHandler.Handle(request) 38}

Finally, we set up the chain of handlers and initiate the request by calling the Handle method of the first handler in the chain:

Go
1func main() { 2 frontDesk := &FrontDeskHandler{} 3 manager := &ManagerHandler{} 4 director := &DirectorHandler{} 5 6 frontDesk.SetNext(manager) 7 manager.SetNext(director) 8 9 request := Request{message: "General inquiry", level: 1} 10 frontDesk.Handle(request) // Output: Request handled by Front Desk 11 12 request = Request{message: "Complaint", level: 2} 13 frontDesk.Handle(request) // Output: Request handled by Manager 14 15 request = Request{message: "High-level issue", level: 3} 16 frontDesk.Handle(request) // Output: Request handled by Director 17}

Notice how the request passes through the chain of handlers until it's processed. This is the essence of the Chain of Responsibility Pattern! As you can see, this pattern allows you to handle requests flexibly and dynamically by passing them through a chain of handlers. Therefore, the first request with level 1 is handled by the FrontDeskHandler, the second request with level 2 is handled by the ManagerHandler as front desk can't handle it, and the third request with level 3 is handled by the DirectorHandler as the manager can't handle it.

Let's understand the key components of the Chain of Responsibility Pattern:

  1. Handler Interface: This interface defines the common methods that all handlers must implement. In our example, the Handle method processes the request, and the SetNext method sets the next handler in the chain.
  2. BaseHandler: This is a base class that implements the Handler interface. It contains the logic to pass the request
  3. Concrete Handlers: These are specific handlers that extend the BaseHandler and implement the Handle method. They decide whether to handle the request or pass it to the next handler in the chain. In our example, we have FrontDeskHandler, ManagerHandler, and DirectorHandler.
  4. Client Code: This is where you set up the chain of handlers and initiate the request. In our example, we create instances of the handlers, link them together, and pass the request to the first handler in the chain.

Let's now understand the flow of the request in our example:

  1. The Request object is passed to the FrontDeskHandler. If the request level matches the handler's level, it handles the request; otherwise, it passes the request to the next handler in the chain.
  2. The ManagerHandler receives the request and checks if it can handle it. If not, it passes the request to the DirectorHandler.
  3. The DirectorHandler processes the request if it matches its level.
  4. If none of the handlers can handle the request, it remains unhandled.
  5. The client code initiates the request by calling the Handle method of the first handler in the chain.
Use Cases of the Chain of Responsibility Pattern

The Chain of Responsibility Pattern is useful in various scenarios, such as:

  1. Request Handling: When you have multiple objects that can handle a request, and you want to pass the request through them until it's processed.
  2. Event Propagation: In user interfaces, you can use this pattern to propagate events through a hierarchy of controls. For example, a button click event can be handled by the button itself, its parent container, and then the main window.
  3. Logging and Error Handling: You can use this pattern to log or handle errors at different levels of your application. For example, a low-level error can be handled by a specific handler, while a high-level error can be processed by a different handler.
Pros and Cons of the Chain of Responsibility Pattern

Pros

  1. Flexibility: You can dynamically change the chain of handlers at runtime.
  2. Decoupling: The sender of the request is decoupled from its receiver, promoting loose coupling.
  3. Maintainability: Adding new handlers to the chain is straightforward, allowing for easy code extension and maintenance.

Cons

  1. Performance Overhead: Passing the request through multiple handlers can impact performance, especially if the chain is long.
  2. Complexity: Managing a long chain of handlers can make the code complex and harder to understand.
  3. Unintended Handling: If the chain is not set up correctly, the request might be handled by an unintended handler.
Why It Matters

Understanding and implementing the Chain of Responsibility Pattern is vital for several reasons:

  1. Flexibility: It allows you to change the chain dynamically at runtime, making your code adaptable to different situations.
  2. Decoupling: By passing a request along the chain, you decouple the sender of the request from its receiver, promoting loose coupling.
  3. Maintainability: Adding new handlers to the chain is straightforward, allowing for easy code extension and maintenance.

Picture a customer service department where requests can be handled at different levels. A simple question might be answered by the front desk, a complaint by a manager, and a serious issue by a director. By using the Chain of Responsibility Pattern, you can easily model this hierarchy in your code.

Excited to see how flexible and powerful this pattern is? Let's dive into the practice section and put this knowledge into action!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.