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
215: Building a Notification System
I've seen this exact scenario play out in a dozen different code reviews. You're building a notification system, and it starts simple—just emails. Then the product manager asks for SMS, then Slack, then maybe Push notifications. You start adding classes, and suddenly you're hitting a wall like this.
class EmailNotification
def send_email(message)
puts "Sending Email: #{message}"
end
end
class SMSNotification
def send_sms(message)
puts "Sending SMS: #{message}"
end
end
# The "Dispatcher" logic
notifications = [EmailNotification.new, SMSNotification.new]
message = "Your order has shipped!"
notifications.each do |notification|
# This is where the crash happens
notification.deliver(message)
end
# Result: NoMethodError (undefined method `deliver' for #<EmailNotification:0x000...>)
The NoMethodError Trap
The mistake here is a classic: the developer created a collection of objects they thought were interchangeable, but they didn't actually share a common interface. In Ruby, we talk a lot about "Duck Typing"—if it walks like a duck and quacks like a duck, it's a duck. But in the code above, one object "quacks" via send_email and the other via send_sms. When the dispatcher tries to call deliver, Ruby looks at the object and says, "I have no idea what that is," and throws a NoMethodError.
You might be tempted to fix this with a big case statement inside the loop, checking the class of each notification. Please, don't do that. That's a maintenance nightmare. Every time you add a new notification channel, you'd have to hunt down every case statement in your codebase and add a new branch.
Standardizing the Interface
The fix is to stop caring about how the notification is sent and instead define a single, consistent method that every notification class must implement. I usually call this the "Contract."
class EmailNotification
def deliver(message)
# Logic for email API goes here
puts "Sending Email: #{message}"
end
end
class SMSNotification
def deliver(message)
# Logic for SMS API goes here
puts "Sending SMS: #{message}"
end
end
class SlackNotification
def deliver(message)
# Logic for Slack Webhook goes here
puts "Posting to Slack: #{message}"
end
end
# Now the dispatcher is blissfully unaware of the specific classes
notifications = [EmailNotification.new, SMSNotification.new, SlackNotification.new]
notifications.each { |n| n.deliver("System Update: Maintenance at Midnight") }
Now, the dispatcher doesn't need to know if it's dealing with an email or a Slack message. It just knows that everything in the notifications array responds to deliver. This is the essence of the Strategy Pattern. It keeps your dispatcher lean and your notification logic isolated.
Handling Failure Gracefully
In a real-world system, external APIs fail. Constantly. If your SMS provider goes down and you're looping through notifications, you don't want a single timeout to crash the entire process and prevent the email from going out. I always recommend wrapping the delivery call in a basic error handler within the loop or the strategy class.
notifications.each do |n|
begin
n.deliver(message)
rescue StandardError => e
# Log the error, but let the loop continue to the next notification
puts "Failed to send #{n.class}: #{e.message}"
end
end
By shifting the responsibility of "how to send" to the individual classes and the responsibility of "what to send" to the dispatcher, you've built a system that is actually extensible. If you need to add WhatsApp notifications tomorrow, you just create a WhatsAppNotification class with a deliver method. You don't have to touch a single line of your existing dispatcher logic.
📋 Practical Task
Build a Multi-Channel Alert Dispatcher for a Stock Tracker
You are building a system that alerts users when a stock hits a certain price. You need to implement a notification system that supports three different channels: Email, SMS, and In-App Notification.
Requirements:
- Create three classes:
EmailAlert,SMSAlert, andInAppAlert. - Each class must implement a
deliver(user, message)method that prints a string indicating the channel used, the user's name, and the alert message. - Create a
NotificationDispatcherclass with a methodsend_all(user, message, channels). This method should take a user object, a message string, and an array of notification channel objects, then trigger the delivery for each. - Ensure that if one channel fails (you can simulate this by making one of your
delivermethods raise aRuntimeError), the other notifications are still sent.
Test your code by: Creating a User object (a simple OpenStruct or a class with a name attribute) and passing it through your dispatcher with all three notification channels.
There are no comments for now.