Welcome back! We are continuing our exploration through Behavioral Patterns with Ruby. Previously, we delved into command-like behavior using Procs and Blocks and examined the Observer pattern. Now, we'll investigate the Strategy Pattern using Ruby's unique object-oriented features and dynamic capabilities.
In this lesson, you'll learn how to implement the Strategy Pattern using Ruby's object-oriented design. We'll simplify the pattern into digestible parts and demonstrate its practical application through a clear example.
Imagine a scenario where you have a ShoppingCart
class, capable of handling payments through different methods like credit cards or PayPal. By employing the Strategy Pattern, we can encapsulate these payment strategies in separate classes and easily switch between them within the ShoppingCart
class.
Ruby leverages its dynamic capabilities to implement the Strategy Pattern using classes without explicit interfaces. We'll start by defining a PaymentStrategy
module providing a common payment method, and subsequently create concrete strategies such as CreditCardStrategy
and PayPalStrategy
that include this module and provide specific behavior:
Ruby1# This module serves as a blueprint for payment strategies, 2# requiring the definition of the `pay` method. 3module PaymentStrategy 4 def pay(amount) 5 # Raises an error if the `pay` method is not implemented by a strategy class. 6 raise NotImplementedError, 'Payment method not implemented' 7 end 8end 9 10# A strategy class that implements payment via Credit Card. 11class CreditCardStrategy 12 include PaymentStrategy 13 14 def initialize(card_number) 15 @card_number = card_number # Stores the credit card number. 16 end 17 18 # Implements the payment logic specific to credit card payment. 19 def pay(amount) 20 puts "Paid #{amount} using Credit Card: #{@card_number}" 21 end 22end 23 24# A strategy class that implements payment via PayPal. 25class PayPalStrategy 26 include PaymentStrategy 27 28 def initialize(email) 29 @user_email = email # Stores the user's PayPal email. 30 end 31 32 # Implements the payment logic specific to PayPal payment. 33 def pay(amount) 34 puts "Paid #{amount} using PayPal: #{@user_email}" 35 end 36end
In these classes, Ruby's modules are used for shared behavior, while individual classes provide their own specific payment logic. The pay
method is a shared interface implemented by the concrete strategy classes.
The ShoppingCart
class will demonstrate how we can set and use a payment strategy in Ruby. This class employs Ruby's dynamic typing and flexibility to change strategies effortlessly:
Ruby1# The context class that utilizes a payment strategy to process payments. 2class ShoppingCart 3 def initialize 4 @strategy = nil # Initializes the payment strategy to nil. 5 end 6 7 # Sets the current payment strategy. 8 def set_payment_strategy(strategy) 9 @strategy = strategy 10 end 11 12 # Executes the payment using the selected strategy. 13 def checkout(amount) 14 if @strategy 15 @strategy.pay(amount) 16 else 17 puts "No payment strategy set." # Alerts if no payment strategy is selected. 18 end 19 end 20end
With this approach, we use instance variables and Ruby's dynamic method invocation to work with the strategy at runtime.
Here's how you can use the ShoppingCart
class with different payment strategies, leveraging Ruby's succinct syntax and paradigms:
Ruby1cart = ShoppingCart.new 2 3credit_card = CreditCardStrategy.new("1234-5678-9876-5432") 4pay_pal = PayPalStrategy.new("user@example.com") 5 6cart.set_payment_strategy(credit_card) 7cart.checkout(100) 8 9cart.set_payment_strategy(pay_pal) 10cart.checkout(200)
This example emphasizes the simplicity with which Ruby can execute different strategies using polymorphism.
The Strategy Pattern in Ruby is particularly effective for scenarios requiring interchangeability of algorithms or behaviors, including:
- Payment Systems: Supporting various payment methods like the example above.
- Sorting Algorithms: Implementing interchangeable sorting methods in an application.
- Compression Libraries: Offering various compression strategies with distinct modules/classes.
- Document Converters: Facilitating multiple document format conversions using separate strategies.
- Game Development: Ascribing different behaviors to game entities that adapt dynamically.
Pros
- Flexibility: Easily switch algorithms/behaviors at runtime.
- Reusability: Strategy classes can be reused across applications.
- Simplified Maintenance: Update algorithms independently of the context class.
- Single Responsibility Principle: Separate classes handle specific behaviors, promoting clearer code.
Cons
- Increased Class Count: More classes can mean more complexity.
- Overhead Management: Frequent changes in strategy may incur performance overhead.
- Client Complexity: Client code must manage and switch strategies.
- Conditional Logic: Debugging and flow understanding might involve more steps due to conditionals.
Understanding the Strategy Pattern in Ruby is vital for building flexible and reusable codebases. Ruby's features like duck typing and mixins allow seamless implementation, making it easy to encapsulate behaviors within distinct classes. This strategy not only enhances maintainability but also ensures that your systems adapt to varying requirements with minimal coding changes. For instance, an e-commerce platform can easily switch between payment options like credit cards or PayPal without altering its core business logic.
By mastering the Strategy Pattern in Ruby, you're empowered to create adaptable systems that remain robust in the face of change. Exciting, isn’t it? Let's proceed to the practice section and implement these concepts!