Replace Subclasses with Strategies⭐
Subclasses are a common method of achieving reuse and polymorphism, but inheritance has its drawbacks. See Composition Over Inheritance⭐ for reasons why you might decide to avoid an inheritance-based model.
During this refactoring, we will replace the subclasses with individual strategy classes. Each strategy class will implement a common interface. The original base class is promoted from an abstract class to the composition root, which composes the strategy classes.
This allows for smaller interfaces, stricter separation of concerns, and easier testing. It also makes it possible to swap out part of the structure, which would require converting to a new type in an inheritance-based model.
When applying this refactoring to an ActiveRecord::Base subclass, STI is re- moved, often in favor of a polymorphic association.
Examples
# subclasses
class Payment
def process(amount)
raise NotImplementedError, 'Subclasses must override this method'
end
end
class CreditCardPayment < Payment
def process(amount)
# Credit card payment processing logic
puts "Processing credit card payment of $#{amount}"
end
end
class PayPalPayment < Payment
def process(amount)
# PayPal payment processing logic
puts "Processing PayPal payment of $#{amount}"
end
end
class BankTransferPayment < Payment
def process(amount)
# Bank transfer payment processing logic
puts "Processing bank transfer payment of $#{amount}"
end
end
# Usage
payments = [CreditCardPayment.new, PayPalPayment.new, BankTransferPayment.new]
payments.each { |payment| payment.process(100) }
⬇️
# Payment strategy interface
class PaymentStrategy
def process(amount)
raise NotImplementedError, 'This method should be overridden in child classes'
end
end
# Concrete strategies
class CreditCardPaymentStrategy < PaymentStrategy
def process(amount)
puts "Processing credit card payment of $#{amount}"
end
end
class PayPalPaymentStrategy < PaymentStrategy
def process(amount)
puts "Processing PayPal payment of $#{amount}"
end
end
class BankTransferPaymentStrategy < PaymentStrategy
def process(amount)
puts "Processing bank transfer payment of $#{amount}"
end
end
class PaymentProcessor
def initialize(strategy)
@strategy = strategy
end
def process_payment(amount)
@strategy.process(amount)
end
end
# Instantiate strategies
credit_card_payment = CreditCardPaymentStrategy.new
paypal_payment = PayPalPaymentStrategy.new
bank_transfer_payment = BankTransferPaymentStrategy.new
# Use strategies with the context
payment_processor = PaymentProcessor.new(credit_card_payment)
payment_processor.process_payment(100)
payment_processor = PaymentProcessor.new(paypal_payment)
payment_processor.process_payment(200)
payment_processor = PaymentProcessor.new(bank_transfer_payment)
payment_processor.process_payment(300)