Behavioural Patterns in Ruby on Rails
🧠 Master Behavioural Patterns in Ruby on Rails: Write Smarter, Cleaner Code! 🚀
Behavioural design patterns are all about how objects interact and communicate with each other. They help you manage complex workflows, decouple components, and make your code more flexible and maintainable. In this blog, we’ll explore key behavioural patterns and show you how to use them effectively in Ruby on Rails. Let’s dive in! 🏊♂️
What Are Behavioural Patterns?
Behavioural patterns focus on improving communication between objects, making your code more modular and easier to extend. They are especially useful in Rails for managing business logic, handling events, and simplifying complex workflows.
Let’s explore the most important behavioural patterns and how to use them in Rails:
1. Observer Pattern: The Event Listener 🎧
What is the Observer Pattern?
The Observer Pattern allows an object (the subject) to notify a list of dependents (observers) about state changes. It’s perfect for decoupling components and handling events.
Why Use It in Rails?
- Keeps your models clean by moving event-handling logic to observers.
- Makes it easy to add or remove observers without modifying the subject.
- Great for sending notifications, logging, or updating related objects.
Example: Notifying Users of a New Comment
Imagine you want to notify users when a new comment is added to a post. Use the Observer Pattern to decouple the notification logic from the Comment
model.
class Comment
attr_reader :post, :user, :content
def initialize(post, user, content)
@post = post
@user = user
@content = content
notify_observers
end
private
def notify_observers
post.notify_observers(self)
end
end
class Post
attr_reader :observers
def initialize
@observers = []
end
def add_observer(observer)
observers << observer
end
def notify_observers(comment)
observers.each { |observer| observer.update(comment) }
end
end
class NotificationObserver
def update(comment)
puts "New comment by #{comment.user}: #{comment.content}"
end
end
# Usage
post = Post.new
post.add_observer(NotificationObserver.new)
Comment.new(post, "Alice", "Great post!")
Pro Tips 💡
- Use Rails’ built-in
ActiveSupport::Notifications
for lightweight event handling. - Combine with background jobs (e.g., Sidekiq) for asynchronous notifications.
- Avoid overusing observers; keep them focused on a single responsibility.
2. Strategy Pattern: The Flexible Algorithm 🎯
What is the Strategy Pattern?
The Strategy Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. It’s ideal for scenarios where you need to switch between different behaviours at runtime.
Why Use It in Rails?
- Makes your code more flexible and extensible.
- Simplifies testing by isolating algorithms.
- Reduces conditional logic in your classes.
Example: Payment Processing
Imagine you have a payment system that supports multiple payment methods (credit card, PayPal, etc.). Use the Strategy Pattern to encapsulate each payment method.
class Payment
attr_reader :strategy
def initialize(strategy)
@strategy = strategy
end
def process(amount)
strategy.process(amount)
end
end
class CreditCardPayment
def process(amount)
puts "Processing credit card payment of $#{amount}"
end
end
class PayPalPayment
def process(amount)
puts "Processing PayPal payment of $#{amount}"
end
end
# Usage
payment = Payment.new(CreditCardPayment.new)
payment.process(100)
payment = Payment.new(PayPalPayment.new)
payment.process(50)
Pro Tips 💡
- Use dependency injection to pass strategies into your classes.
- Combine with Rails’
ActiveSupport::Configurable
for dynamic configuration. - Use strategies to handle feature toggles or A/B testing.
3. Command Pattern: The Action Encapsulator 📦
What is the Command Pattern?
The Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
Why Use It in Rails?
- Decouples the sender of a request from its executor.
- Simplifies adding new commands without modifying existing code.
- Great for implementing undo/redo functionality or queuing tasks.
Example: Undoable Actions
Imagine you want to implement an undo feature for a text editor. Use the Command Pattern to encapsulate each action.
class TextEditor
attr_accessor :text
def initialize
@text = ""
end
end
class Command
attr_reader :editor, :backup
def initialize(editor)
@editor = editor
end
def execute
raise NotImplementedError
end
def undo
editor.text = backup
end
def save_backup
@backup = editor.text.dup
end
end
class AddTextCommand < Command
def initialize(editor, text)
super(editor)
@text = text
end
def execute
save_backup
editor.text += @text
end
end
# Usage
editor = TextEditor.new
command = AddTextCommand.new(editor, "Hello, world!")
command.execute
puts editor.text # => "Hello, world!"
command.undo
puts editor.text # => ""
Pro Tips 💡
- Use commands to implement background jobs or task queues.
- Combine with the Composite Pattern to create macro commands.
- Use commands to implement transactional operations.
4. Chain of Responsibility: The Responsibility Delegator ⛓️
What is the Chain of Responsibility?
The Chain of Responsibility Pattern allows you to pass a request along a chain of handlers. Each handler either processes the request or passes it to the next handler in the chain.
Why Use It in Rails?
- Decouples the sender of a request from its handlers.
- Simplifies adding or removing handlers.
- Great for middleware, logging, or validation pipelines.
Example: Request Validation
Imagine you want to validate a user registration request. Use the Chain of Responsibility to handle each validation step.
class Handler
attr_accessor :next_handler
def handle(request)
if can_handle?(request)
process(request)
elsif next_handler
next_handler.handle(request)
else
puts "Request cannot be handled."
end
end
def can_handle?(request)
raise NotImplementedError
end
def process(request)
raise NotImplementedError
end
end
class EmailValidator < Handler
def can_handle?(request)
request[:email].include?("@")
end
def process(request)
puts "Email is valid."
end
end
class PasswordValidator < Handler
def can_handle?(request)
request[:password].length >= 8
end
def process(request)
puts "Password is valid."
end
end
# Usage
email_validator = EmailValidator.new
password_validator = PasswordValidator.new
email_validator.next_handler = password_validator
request = { email: "test@example.com", password: "password123" }
email_validator.handle(request)
Pro Tips 💡
- Use the Chain of Responsibility for middleware or request processing.
- Combine with Rails’
ActiveSupport::Callbacks
for reusable hooks. - Avoid creating long chains; keep them focused and manageable.
Excited Tips to Improve Code Efficiency and Readability 🌟
- Keep It DRY: Use patterns to avoid duplicating logic.
- Use Meaningful Names: Name your observers, strategies, and commands descriptively.
- Leverage Rails Conventions: Use Rails’ built-in tools to simplify pattern implementation.
- Test Thoroughly: Write unit tests for your patterns to ensure they behave as expected.
- Document Your Patterns: Add comments or READMEs to explain how and why you’re using these patterns.
Conclusion: Write Smarter with Behavioural Patterns 🧠
Behavioural patterns like Observer, Strategy, Command, and Chain of Responsibility are powerful tools in your Rails toolkit. They help you manage complex workflows, decouple components, and make your code more flexible and maintainable. Start using these patterns today, and watch your Rails applications soar to new heights! 🚀
What’s Next?
Stay tuned for our next blog in the series, where we’ll dive into Structural Patterns like Decorator and Adapter! 🎉
Let’s Connect!
If you found this blog helpful, share it with your fellow developers and leave a comment below. What’s your favourite behavioural pattern? Let’s discuss! 💬
Happy Coding! 💻✨
© Lakhveer Singh Rajput - Blogs. All Rights Reserved.