Table of contents
    blog cover

    One Design Pattern a Week: Week 4

    Software Engineer
    Software Engineer
    Design Pattern
    Design Pattern
    Welcome back to my "One Design Pattern a Week" series!

    Try to solve this real problem: Request Handling

    Imagine you're building an enterprise application that needs to process different types of requests (like approvals, validations, or authentications). Each request might need to go through multiple processing steps, and different requests might require different combinations of these steps.

    Here are some potential issues:
    • Complex Decision Logic: Without proper structure, the code becomes filled with nested if-else statements
    • Tight Coupling: Processing steps are tightly coupled, making it difficult to add or remove steps
    • Code Maintainability: Changes to the processing logic require modifying existing code

    Take a moment to think about how you would solve this problem. How can you create a flexible system that can handle different types of requests with varying processing requirements?

    An example of bad design

    // language: ruby
    class RequestProcessor
      def process_request(request)
        # Check authentication
        if !authenticate(request)
          return "Authentication failed"
        end
    
        # Validate request
        if !validate(request)
          return "Validation failed"
        end
    
        # Check authorization
        if !authorize(request)
          return "Authorization failed"
        end
    
        # Process the request
        "Request processed successfully"
      end
    
      private
    
      def authenticate(request)
        # Authentication logic
        true
      end
    
      def validate(request)
        # Validation logic
        true
      end
    
      def authorize(request)
        # Authorization logic
        true
      end
    end
    
    # Usage
    processor = RequestProcessor.new
    puts processor.process_request("some request")

    Issues with this implementation:
    1. Rigid Structure: The processing steps are hardcoded and cannot be easily modified
    2. Single Responsibility Principle Violation: The RequestProcessor class handles multiple responsibilities
    3. Poor Extensibility: Adding new processing steps requires modifying existing code

    Chain of Responsibility Pattern

    The Chain of Responsibility pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process it or to pass it along the chain.

    Key Characteristics:
    • Decoupling: Separates senders of requests from receivers
    • Dynamic Chain: Handlers can be added or removed dynamically
    • Single Responsibility: Each handler focuses on one specific task

    Implementing Chain of Responsibility in Ruby:
    // language: ruby
    # Abstract Handler
    class Handler
      attr_writer :next_handler
    
      def handle(request)
        return @next_handler.handle(request) if @next_handler
        nil
      end
    end
    
    # Concrete Handlers
    class AuthenticationHandler < Handler
      def handle(request)
        if authenticate(request)
          puts "Authentication successful"
          super(request)
        else
          "Authentication failed"
        end
      end
    
      private
    
      def authenticate(request)
        # Authentication logic
        true
      end
    end
    
    class ValidationHandler < Handler
      def handle(request)
        if validate(request)
          puts "Validation successful"
          super(request)
        else
          "Validation failed"
        end
      end
    
      private
    
      def validate(request)
        # Validation logic
        true
      end
    end
    
    class AuthorizationHandler < Handler
      def handle(request)
        if authorize(request)
          puts "Authorization successful"
          super(request)
        else
          "Authorization failed"
        end
      end
    
      private
    
      def authorize(request)
        # Authorization logic
        true
      end
    end
    
    # Client code
    authentication = AuthenticationHandler.new
    validation = ValidationHandler.new
    authorization = AuthorizationHandler.new
    
    # Set up the chain
    authentication.next_handler = validation
    validation.next_handler = authorization
    
    # Process request
    result = authentication.handle("some request")
    puts result

    Benefits of this implementation:
    • Flexible Chain Construction: Handlers can be arranged in any order
    • Easy to Add/Remove Steps: New handlers can be added without modifying existing code
    • Single Responsibility: Each handler focuses on one specific task
    • Loose Coupling: Handlers are independent of each other

    More Practical Examples

    1. HTTP Request Middleware Chain
    // language: ruby
    # Base Middleware Handler
    class Middleware
      attr_writer :next_handler
    
      def handle(request)
        return @next_handler.handle(request) if @next_handler
        request
      end
    end
    
    # CORS Middleware
    class CORSMiddleware < Middleware
      def handle(request)
        # Add CORS headers
        request.headers['Access-Control-Allow-Origin'] = '*'
        request.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
        
        puts "CORS headers added to request"
        super(request)
      end
    end
    
    # Authentication Middleware
    class JWTAuthMiddleware < Middleware
      def handle(request)
        token = request.headers['Authorization']
        
        if token.nil?
          return { status: 401, message: 'No token provided' }
        end
    
        begin
          decoded_token = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256')
          request.user = decoded_token['user']
          puts "User authenticated successfully"
          super(request)
        rescue JWT::DecodeError
          { status: 401, message: 'Invalid token' }
        end
      end
    end
    
    # Rate Limiting Middleware
    class RateLimitMiddleware < Middleware
      def initialize
        @redis = Redis.new
        @max_requests = 100  # 100 requests
        @time_window = 3600  # per hour
      end
    
      def handle(request)
        client_ip = request.ip
        current_count = @redis.get(client_ip).to_i
    
        if current_count >= @max_requests
          return { status: 429, message: 'Rate limit exceeded' }
        end
    
        @redis.multi do
          @redis.incr(client_ip)
          @redis.expire(client_ip, @time_window)
        end
    
        super(request)
      end
    end
    
    # Usage
    class Request
      attr_accessor :headers, :ip, :user
      
      def initialize
        @headers = {}
        @ip = '127.0.0.1'
      end
    end
    
    # Set up middleware chain
    rate_limit = RateLimitMiddleware.new
    cors = CORSMiddleware.new
    auth = JWTAuthMiddleware.new
    
    rate_limit.next_handler = cors
    cors.next_handler = auth
    
    # Process request
    request = Request.new
    request.headers['Authorization'] = 'valid.jwt.token'
    result = rate_limit.handle(request)

    Conclusion

    The Chain of Responsibility pattern provides a flexible and maintainable way to process requests through a series of handlers. It promotes loose coupling, single responsibility, and easy modification of processing chains.

    Happy coding!
    Created at 2024-10-28 16:46:15 +0700

    Related blogs