Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
17: Duck Typing in Ruby
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:
- Create three classes:
StripeGateway,PayPalGateway, andSquareGateway. - Each class should have a method called
process_payment(amount)that prints a message like:"Processing $[amount] via Stripe". - Create a
PaymentProcessorclass with a methodexecute_transaction(gateway, amount). - Inside
execute_transaction, do not useis_a?or any class-specific checks. Use duck typing to trigger the payment. - Test your
PaymentProcessorby passing it instances of all three gateways in a loop. - Bonus: Try passing a plain string or integer into
execute_transactionand userespond_to?to handle the error gracefully without the program crashing.
There are no comments for now.