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)