0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
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:
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:
- Rigid Structure: The processing steps are hardcoded and cannot be easily modified
- Single Responsibility Principle Violation: The RequestProcessor class handles multiple responsibilities
- 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:
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 resultBenefits 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!
Happy coding!
Related blogs
Speed Up Independent Queries Using Rails load_async
When you're building a dashboard, it's common to fetch multiple, independent datasets. The page loading might be slow because it has to fetch all data to render a page. A common solution is using AJAX to load pieces of the dashboard, which is great, ...
Software Engineer
Software Engineer
Hello Golang: My First Steps to the Language
I’ve worked with Ruby in several projects, which is defined as "a programmer’s best friend" for its clean and English-like syntax. While my back-end experience is rooted in the Ruby on Rails framework, I prefer TypeScript for building CLI tools and s...
Software Engineer
Software Engineer