Deep Dive into Behavioral Patterns - The Chain of Responsibility Pattern.

Hey software designers! Today, we’re diving into the Chain of Responsibility pattern. This pattern is essential for creating a chain of processing objects where each object in the chain handles a request or passes it to the next object in the chain. Let’s explore its workings, benefits, and real-world applications with detailed examples.

What is the Chain of Responsibility Pattern?

The Chain of Responsibility pattern is a behavioral design pattern that allows an object to pass a request along a chain of potential handlers until the request is handled. It decouples the sender of the request from its receiver by allowing multiple objects to handle the request without the sender knowing which object will handle it.

Real-World Scenario

Imagine you’re developing a help desk ticketing system where different levels of support handle different types of issues. A customer service representative handles basic issues, a technical support representative handles more complex issues, and a manager handles the most critical issues. Each issue needs to be escalated to the appropriate level of support based on its complexity.

The Problem

When processing requests that can be handled by multiple handlers, hardcoding the logic to determine which handler should process each request can lead to a tightly coupled and inflexible codebase. This approach makes it difficult to add or modify handlers without changing the existing code.

Without Chain of Responsibility Pattern

class HelpDesk
  def handle_request(issue)
    if issue.type == 'basic'
      handle_basic_issue(issue)
    elsif issue.type == 'technical'
      handle_technical_issue(issue)
    elsif issue.type == 'critical'
      handle_critical_issue(issue)
    else
      puts 'Unknown issue type'
    end
  end

  def handle_basic_issue(issue)
    puts "Handling basic issue: #{issue.description}"
  end

  def handle_technical_issue(issue)
    puts "Handling technical issue: #{issue.description}"
  end

  def handle_critical_issue(issue)
    puts "Handling critical issue: #{issue.description}"
  end
end

issue1 = OpenStruct.new(type: 'basic', description: 'Password reset')
issue2 = OpenStruct.new(type: 'technical', description: 'Software installation')
issue3 = OpenStruct.new(type: 'critical', description: 'System outage')

help_desk = HelpDesk.new
help_desk.handle_request(issue1)
help_desk.handle_request(issue2)
help_desk.handle_request(issue3)

Drawbacks: The code is tightly coupled to specific implementations and hard to extend to support new issue types.

The Solution: Chain of Responsibility Pattern

Using the Chain of Responsibility pattern, we can create a chain of handlers that process the request or pass it to the next handler in the chain, promoting flexibility and scalability.

With Chain of Responsibility Pattern

Step 1: Define the Handler Interface

class Handler
  def initialize(successor = nil)
    @successor = successor
  end

  def handle_request(issue)
    if @successor
      @successor.handle_request(issue)
    else
      puts 'Request not handled'
    end
  end
end

Step 2: Create Concrete Handlers

class CustomerServiceHandler < Handler
  def handle_request(issue)
    if issue.type == 'basic'
      puts "Handling basic issue: #{issue.description}"
    else
      super(issue)
    end
  end
end

class TechnicalSupportHandler < Handler
  def handle_request(issue)
    if issue.type == 'technical'
      puts "Handling technical issue: #{issue.description}"
    else
      super(issue)
    end
  end
end

class ManagerHandler < Handler
  def handle_request(issue)
    if issue.type == 'critical'
      puts "Handling critical issue: #{issue.description}"
    else
      super(issue)
    end
  end
end

Step 3: Implement Client Code

manager = ManagerHandler.new
technical_support = TechnicalSupportHandler.new(manager)
customer_service = CustomerServiceHandler.new(technical_support)

issue1 = OpenStruct.new(type: 'basic', description: 'Password reset')
issue2 = OpenStruct.new(type: 'technical', description: 'Software installation')
issue3 = OpenStruct.new(type: 'critical', description: 'System outage')

customer_service.handle_request(issue1)
customer_service.handle_request(issue2)
customer_service.handle_request(issue3)

Benefits: Decouples the sender of the request from its receiver, promoting flexibility and scalability.

Real-World Benefits

Scenario: Adding New Issue Types

Imagine you need to add a new issue type (e.g., ‘billing’). Using the Chain of Responsibility pattern, you can easily introduce a new handler for billing issues without modifying the existing code.

Without Chain of Responsibility Pattern:

class HelpDesk
  def handle_request(issue)
    if issue.type == 'basic'
      handle_basic_issue(issue)
    elsif issue.type == 'technical'
      handle_technical_issue(issue)
    elsif issue.type == 'critical'
      handle_critical_issue(issue)
    elsif issue.type == 'billing'
      handle_billing_issue(issue)
    else
      puts 'Unknown issue type'
    end
  end

  def handle_basic_issue(issue)
    puts "Handling basic issue: #{issue.description}"
  end

  def handle_technical_issue(issue)
    puts "Handling technical issue: #{issue.description}"
  end

  def handle_critical_issue(issue)
    puts "Handling critical issue: #{issue.description}"
  end

  def handle_billing_issue(issue)
    puts "Handling billing issue: #{issue.description}"
  end
end

Drawbacks: Tightly coupled code that is difficult to maintain and extend.

With Chain of Responsibility Pattern:

class BillingHandler < Handler
  def handle_request(issue)
    if issue.type == 'billing'
      puts "Handling billing issue: #{issue.description}"
    else
      super(issue)
    end
  end
end

billing = BillingHandler.new(manager)
technical_support = TechnicalSupportHandler.new(billing)
customer_service = CustomerServiceHandler.new(technical_support)

issue4 = OpenStruct.new(type: 'billing', description: 'Invoice discrepancy')

customer_service.handle_request(issue4)

Benefits: Clean, maintainable code with high flexibility and extensibility.

Conclusion

The Chain of Responsibility pattern is a powerful tool for creating a chain of processing objects where each object in the chain handles a request or passes it to the next object in the chain. It promotes flexibility, scalability, and maintainability in your code. By using the Chain of Responsibility pattern, you can easily manage requests that can be handled by multiple handlers without tightly coupling the sender to the receivers. Incorporate the Chain of Responsibility pattern into your design strategies to build more robust and adaptable software systems.

Stay tuned for more insights into software design principles and patterns.

Thôi Lo Code Đi Kẻo Sếp nạt!