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
52: The Observer Pattern in Ruby
Imagine we're building a weather monitoring system. We have a WeatherStation that reads data from a sensor, and we want several different displays—like a phone app and a wall-mounted LED screen—to update whenever the temperature changes. My first instinct, if I'm rushing, is to just tell the station exactly who needs the update.
The Coupling Trap
Let's look at how I'd probably write this if I wasn't thinking about design patterns. I'll just pass the displays into the station and call a method on them.
class WeatherStation
def initialize(phone_app, wall_display)
@phone_app = phone_app
@wall_display = wall_display
@temp = 0
end
def set_temperature(new_temp)
@temp = new_temp
@phone_app.update(@temp)
@wall_display.update(@temp)
end
end
class PhoneApp
def update(temp)
puts "Phone App: The temperature is now #{temp}°C"
end
end
class WallDisplay
def update(temp)
puts "Wall Display: Showing #{temp}°C"
end
end
station = WeatherStation.new(PhoneApp.new, WallDisplay.new)
station.set_temperature(25)
This works, but look at the WeatherStation. It's "coupled" to the phone app and the wall display. What happens if we add a WebDashboard? Or a LogFile? I have to keep changing the initialize method and the set_temperature method every time we add a new device. That's a maintenance nightmare. I don't want the station to care who is listening; I just want it to shout, "Hey, the temp changed!" to whoever is interested.
Attempting a Manual Registry
Okay, I can fix this by using a list. Instead of specific objects, I'll just keep an array of "observers."
class WeatherStation
def initialize
@observers = []
end
def add_observer(observer)
@observers <= observer
end
def set_temperature(new_temp)
@temp = new_temp
@observers.each { |obs| obs.update(@temp) }
end
end
This is better. Now the WeatherStation doesn't need to know the names of the classes it's talking to. But I'm starting to realize I'm writing a lot of "plumbing" code. I'm managing lists, iterating through them, and handling registrations. This feels like a problem Ruby has probably already solved.
Leveraging the Observable Module
Ruby actually provides a built-in module called Observable. It handles the registration and notification logic for us. Let's see how it changes the implementation. I'll need to require 'observer' first.
require 'observer'
class WeatherStation
include Observable
def set_temperature(new_temp)
@temp = new_temp
# This is the part that trips people up
changed
notify_observers(@temp)
end
end
class PhoneApp
def update(temp)
puts "Phone App: The temperature is now #{temp}°C"
end
end
station = WeatherStation.new
station.add_observer(PhoneApp.new)
station.set_temperature(22)
Wait, why did I call changed? If I remove that line, notify_observers does absolutely nothing. I found this out the hard way the first time I used this module.
The logic is this: the Observable module wants to prevent unnecessary updates. If you call set_temperature(20) and the temperature was already 20, you might not want to trigger a bunch of expensive API calls in your observers. By calling changed, you are explicitly flagging the object as "dirty." notify_observers checks that flag; if it's true, it alerts everyone and then resets the flag to false.
Refining the Flow
Now we have a clean separation. The WeatherStation just manages the data and the "dirty" flag. The observers just implement an update method. I can add ten different observers—email alerts, database loggers, sirens—and I never have to touch the WeatherStation code again. That's the core of the Observer pattern: the subject doesn't know the details of its observers, only that they respond to a specific method.
📋 Practical Task
Exercise: Building a Multi-Channel Notification System for an E-commerce Order
You are building an order processing system. When an order's status changes (e.g., from "Pending" to "Shipped"), multiple systems need to react: an EmailNotifier needs to email the customer, and an InventoryManager needs to update the stock levels.
Your Task:
- Create an
Orderclass that includes theObservablemodule. - Implement a
status=`method in theOrderclass that marks the object as changed and notifies observers whenever the status is updated. - Create an
EmailNotifierclass with anupdate(status)method that prints:"Sending email: Your order is now #{status}!" - Create an
InventoryManagerclass with anupdate(status)method that prints:"Updating inventory for status: #{status}" - Instantiate an
Order, attach both observers to it, and change the status to "Shipped" to trigger the notifications.
There are no comments for now.