The Observer Pattern is a convenient way to update several objects when one object changes state. For example, let’s say we have a website that tracks the status of real estate properties. A property can go through several different states such as “coming soon”, “for sale”, “pending sale”, and “sold.”

class Property
  attr_reader :street_address, :price, :sale_status

  def initialize(street_address:, price:, sale_status:)
    @street_address = street_address
    @price = price
    @sale_status = sale_status
  end
end

There are also a few different parties that may be interested in to know when that state changes, for example realtors, owners, potential buyers, tax entities, and banks. We’ll start with just Realtors:

class Realtor
end

A Property doesn’t care who follows it

Do you think Beyonce sends individual messages to every single one of her followers when she’s going to be touring in their cities? Nope. She posts that information once and the people who subscribe to those notifications get those notifications. Our property feels the same way (though between you and me, our property is no Beyonce).

property = Property.new(
  street_address: '123 Main Street',
  price: '300,000',
  sale_status: 'coming soon'
)

property.tell_everybody_everything  # <= Um, no. We're not doing that.

This is not a property’s job. A property’s job to is to know about property stuff. That is all. But a property is willing to permit observers to “follow” it, or “subscribe” to certain state changes. We do this by initializing it with an observers attribute.

class Property
  attr_reader :street_address, :price, :sale_status

  def initialize(street_address:, price:, sale_status:)
    @street_address = street_address
    @price = price
    @sale_status = sale_status
    @observers = []  # <= Empty array for storing observers
  end
end

And then we give the class some methods to interact with this array of observers:


# Methods for interacting with obeservers
def add_observer(observer)
  @observers << observer
end

def remove_observer(observer)
  @observers.delete(observer)
end

def notify_observers
  @observers.each do |observer|
    observer.update(self)
  end
end

The Property decides when to notify observers

Beyonce doesn’t tell you everything. Neither does our property. Let’s notify our observers when the sale_status changes:

def sale_status=(new_status)
  @sale_status = new_status
  notify_observers
end

Now we give the property an observer that is a Realtor:

realtor = Realtor.new
property.add_observer(realtor)

The observing object decides how to react

Give the observing object something to do when it gets notified:

class Realtor
  def update(property)
    puts "#{self.class} says: Hello Commission! The property at #{property.street_address} is now #{property.sale_status}."
  end
end

The property just does it’s thing

Now when the sale_status changes, the observer pattern is triggered and all of the observers get notified:

property.sale_status = 'sold'
# => Realtor says: Hello Commission! The property at 123 Main Street is now sold.

People’ve been smashing that subscribe button…

Let’s say this property has even more observers.

class PotentialBuyerNews
  def update(property)
    puts "#{self.class} says: The listing you're following at #{property.street_address} is now #{property.sale_status}."
  end
end

class TaxEntity
  def update(property)
    puts "#{self.class} says: Send a sales tax bill to #{property.street_address} on a value of $#{property.price}."
  end
end

class Bank
  def update(property)
    puts "#{self.class} says: Start racking up interest on $#{property.price} for #{property.street_address}."
  end
end

We can add them all at once:

class Property
  def bulk_add_observers(observers)
    @observers << observers
    @observers.flatten!
  end
end

realtor = Realtor.new
potential_buyer_news = PotentialBuyerNews.new
tax_entity = TaxEntity.new
bank = Bank.new

property.bulk_add_observers([
  realtor,
  potential_buyer_news,
  tax_entity,
  bank
])

Again, the property doesn’t need to do any more work when more observers are added, but the effects ripple outward:

# The property updates its status once and...
property.sale_status = 'sold'

# we get output from all of the observers:
"Realtor says: Oh snap! The property at 123 Main Street is now sold."
"PotentialBuyerNews says: The listing you're following at 123 Main Street is now sold."
"TaxEntity says: Send a sales tax bill to 123 Main Street on a value of $300,000."
"Bank says: Start racking up interest on $300,000 for 123 Main Street."

That’s pretty nifty.

Refactor Extractor

Now that our observer is in place and working well, let’s pull out all of those observer-related methods into their own module:

module Notifier
  def add_observer(observer)
    @observers << observer
  end

  def bulk_add_observers(observers)
    @observers << observers
    @observers.flatten!
  end

  def remove_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end

And then include the module in the Property class:

class Property
  include Notifier

  attr_reader :street_address, :price, :sale_status
  ...

Now our Property class stays cleaner and the observer functions all live in one logical place.

The Ruby language is down with this pattern

Ruby likes a good old fashioned observer pattern and makes it a little easier for us by providing an Observable module that includes some of the functionality that we built here in our Notifier module. This Medium post does a nice job of incorporating Ruby’s Observable module into an example.

If you dig reading about patterns like this, check out Design Patterns in Ruby by Russ Olsen. That’s where I learned about this pattern and a few others.