Skip to Content
Course content

52: The Observer Pattern in Ruby

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

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 Order class that includes the Observable module.
  • Implement a status=` method in the Order class that marks the object as changed and notifies observers whenever the status is updated.
  • Create an EmailNotifier class with an update(status) method that prints: "Sending email: Your order is now #{status}!"
  • Create an InventoryManager class with an update(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.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.