Skip to Content
Course content

17: Duck Typing in Ruby

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

I was working on a notification system the other day and I caught myself doing something that I’ve seen a lot of developers do when they come from languages like Java or C#. I was trying to be "safe" by checking exactly what kind of object I was dealing with before calling a method on it. It felt like the right thing to do at the time, but it actually made my code a nightmare to maintain.

The "Safe" Way (That Actually Isn't)

Here is where I started. I had an EmailService and I wanted a NotificationDispatcher to handle sending the actual messages. I wrote it like this:

class EmailService
  def send_msg(text)
    puts "Sending Email: #{text}"
  end
end

class NotificationDispatcher
  def dispatch(service, message)
    if service.is_a?(EmailService)
      service.send_msg(message)
    else
      raise "I don't know how to use this service!"
    end
  end
end

email = EmailService.new
dispatcher = NotificationDispatcher.new
dispatcher.dispatch(email, "Hello there!") # Works great.

At first glance, this is logical. I'm ensuring the service is an EmailService before I call send_msg. But then my boss tells me we need to support SMS notifications. I add an SmsService, and suddenly my dispatch method starts looking like a giant, ugly if/elsif chain.

class SmsService
  def send_msg(text)
    puts "Sending SMS: #{text}"
  end
end

# Now I have to change the dispatcher...
def dispatch(service, message)
  if service.is_a?(EmailService)
    service.send_msg(message)
  elsif service.is_a?(SmsService)
    service.send_msg(message)
  else
    raise "Unsupported service!"
  end
end

I stopped and looked at that code. I'm literally doing the exact same thing—calling send_msg—regardless of whether it's an email or an SMS. The NotificationDispatcher doesn't actually care what the object is; it only cares that the object responds to send_msg.

Trusting the Object

This is the core of "Duck Typing." The phrase comes from the old adage: "If it walks like a duck and it quacks like a duck, then it probably is a duck." In Ruby, we don't ask an object for its ID card (its class) before we ask it to do something. We just tell it to do the thing and assume it knows how.

Let's strip away all that type-checking nonsense and see what happens:

class NotificationDispatcher
  def dispatch(service, message)
    service.send_msg(message)
  end
end

# Now look what happens:
dispatcher = NotificationDispatcher.new
dispatcher.dispatch(EmailService.new, "Hi via Email!")
dispatcher.dispatch(SmsService.new, "Hi via SMS!")

It works perfectly. And the best part? If I want to add a SlackService or a PagerDutyService tomorrow, I don't have to touch a single line of code in the NotificationDispatcher. As long as the new class has a send_msg method, the dispatcher will happily use it.

When the Duck Doesn't Quack

You might be thinking, "But what if I pass in an object that doesn't have that method? Won't the program crash?" Yes, it will. You'll get a NoMethodError. In many languages, that's a terrifying prospect, but in Ruby, it's often the desired behavior. If you pass a Integer into a notification dispatcher, something is fundamentally wrong with your logic, and the program should fail loudly so you can fix it.

However, if you're dealing with unpredictable data and you really aren't sure if the object can handle the request, Ruby gives you a way to check without locking yourself into a specific class. I use respond_to? for this:

def dispatch(service, message)
  if service.respond_to?(:send_msg)
    service.send_msg(message)
  else
    puts "This object can't send messages. I'll just ignore it."
  end
end

Notice the difference. I'm still not asking "Are you an EmailService?" I'm asking "Do you have a method called send_msg?" This keeps the code flexible. I'm focusing on behavior rather than inheritance.




📋 Practical Task

Exercise: Building a Universal Payment Processor

You are building a checkout system that needs to support multiple payment gateways (e.g., Stripe, PayPal, and Square). Each gateway has a different internal API, but your system should be able to process a payment regardless of which provider is being used.

Your Task:

  1. Create three classes: StripeGateway, PayPalGateway, and SquareGateway.
  2. Each class should have a method called process_payment(amount) that prints a message like: "Processing $[amount] via Stripe".
  3. Create a PaymentProcessor class with a method execute_transaction(gateway, amount).
  4. Inside execute_transaction, do not use is_a? or any class-specific checks. Use duck typing to trigger the payment.
  5. Test your PaymentProcessor by passing it instances of all three gateways in a loop.
  6. Bonus: Try passing a plain string or integer into execute_transaction and use respond_to? to handle the error gracefully without the program crashing.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.