Skip to Content
Course content

15: Inheritance and Polymorphism

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely reached a point in your project where you're starting to see patterns. You have a User class, an Admin class, and a Moderator class. They all have names, emails, and passwords, but they do slightly different things. Your first instinct is probably to copy-paste the common methods into all three. Don't do that. It's a maintenance nightmare.

Let's look at a piece of code I reviewed recently. The developer was building a payment system and tried to use inheritance, but they hit a wall that almost every Rubyist hits early on.

class Payment
  def process(amount)
    puts "Logging transaction of $#{amount} to the database..."
    # Imagine complex logging logic here
  end
end

class CreditCardPayment < Payment
  def process(amount)
    puts "Charging $#{amount} to Credit Card."
  end
end

class PayPalPayment < Payment
  def process(amount)
    puts "Redirecting to PayPal for $#{amount}."
  end
end

# Testing the code
payments = [CreditCardPayment.new, PayPalPayment.new]
payments.each { |p| p.process(100) }

The disappearing log entries

At first glance, this looks fine. It's clean, and it works. But if you run this, you'll notice something is missing: the "Logging transaction..." message from the Payment class never appears. The developer was confused because they inherited from Payment, so they assumed the base logic would just "happen" automatically.

Here is the reality: when you define a method in a subclass with the exact same name as a method in the parent class, you aren't adding to the method—you are completely replacing it. In Ruby, this is called method overriding. The CreditCardPayment#process method effectively nuked the Payment#process method for that object.

Restoring the chain with super

To fix this, we need a way to tell Ruby, "Do the stuff in the subclass, but also do the stuff the parent class knows how to do." That's exactly what the super keyword is for.

class Payment
  def process(amount)
    puts "Logging transaction of $#{amount} to the database..."
  end
end

class CreditCardPayment < Payment
  def process(amount)
    super # This calls the process method of the parent (Payment)
    puts "Charging $#{amount} to Credit Card."
  end
end

class PayPalPayment < Payment
  def process(amount)
    super
    puts "Redirecting to PayPal for $#{amount}."
  end
end

Now, when you call process on a CreditCardPayment object, Ruby jumps to the subclass method, sees super, jumps up to the parent class to run the logging logic, and then comes back down to finish the credit card specific logic. I usually put super at the top if the parent handles setup or logging, but you can put it at the bottom if the parent handles the final cleanup.

Handling variety through polymorphism

Now that we have inheritance working, we can unlock Polymorphism. Don't let the Greek name intimidate you; it just means "many shapes." In practice, it means we can treat different objects as if they are the same type, as long as they respond to the same method.

Look back at how we iterated through the payments array: payments.each { |p| p.process(100) }. The loop doesn't know if p is a CreditCardPayment or a PayPalPayment. It only knows that p is some kind of Payment and that it has a process method.

This is the "secret sauce" of scalable software. If you decide to add CryptoPayment or ApplePayPayment next week, you don't have to change the loop that processes payments. You just create the new class, inherit from Payment, and implement process. The rest of your system just works.

The trap of the fragile base class

A word of caution from someone who has broken too many production apps: be careful how much logic you put in the parent class. When you put a method in the base class, every single subclass inherits it. If you change a method in Payment to fix a bug for Credit Cards, you might accidentally break PayPal.

If you find yourself writing if self.is_a?(CreditCardPayment) inside the parent class, stop immediately. You're fighting against polymorphism. Instead, move that specific logic into the subclass and let the parent class stay generic.




📋 Practical Task

Build a Multi-Channel Notification System

You need to build a system that sends notifications to users via different channels. Some users prefer Email, some prefer SMS, and some prefer Push notifications.

  • Create a base class called Notifier with a method send_notification(message) that prints "Preparing system for transmission...".
  • Create three subclasses: EmailNotifier, SMSNotifier, and PushNotifier.
  • Each subclass should override send_notification. It must call super to ensure the preparation message is printed, and then print its own specific message (e.g., "Sending Email: [message]").
  • Create an array containing one instance of each notifier.
  • Iterate through the array and send the message "Your order has shipped!" using each notifier.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.