I See You: Intro to Observer Pattern

Observer pattern is a behavioral design pattern that attempts to resolve the problem of multiple objects depending on the state of a single object. Here’s an overview, and a somewhat simplified example.

Observer Pattern Basic Overview

In Observer Pattern, there is a subject object that manages all the data, and observer objects that get notified when the data in the subject changes. The subject can be watched by one or more observers and can notify its observers when there are state changes.

The subject will have methods add() and remove() for maintaining an internal list of its observers, and a method like notify() for alerting its observers of a change.

The observers will each have a method update() for subject.notify() to call when there is a state change.

class DemoSubject
  def initialize
    @observers = []
  end

	def add(observer)
    @observers << observer
  end

  def remove(observer)
    @observer.delete(observer)
  end

  def notify
    @observers.each do |observer|
			observer.update
    end
  end

  ...
end

class DemoObserver
  def update
    ...
  end
end

When an observer is created, it is registered to the subject’s internal list of observers by calling subject.add(new_observer).

There are two ways to implement what happens when there is a state change. You can either have the subject push data to the observers or have the observers pull data from the subject.

Pushing means the subject will pass along data to the observers when calling each observer’s update(): observer.update(some_data)

Pulling means the subject does not pass along any data when calling the observer’s update() method. Instead, the observer’s update() is responsible for retrieving the necessary data from the subject.

While pushing can be more efficient (i.e. fewer method calls), you must pass the same thing to every observer, whereas pulling can specifically retrieve only the data the observer needs. Both have their benefits. Let’s look at some examples.

Simplified Example (plus disco lights)

Imagine, if you will, that we are making a rocket ship. And a rocket ship needs fuel to fly, and maybe do other things that rocket ships do.

# The fuel monitor is the subject
class FuelMonitor
  def initialize
    @observers = []
  end

  def add(observer)
    @observers << observer
  end

  def remove(observer)
    @observer.delete(observer)
  end
 
  def notify
    @observers.each do |observer|
	  observer.update
    end
  end

  def fuel_level
    ...
  end

  def low_fuel?
	...
  end

  ...
end

There are many things dependent on the state of the fuel. There probably is a dashboard display where a crew member can keep an eye on how much fuel is left.

class DashboardDisplay
  def initialize(fuel_monitor)
    @fuel_monitor = fuel_monitor
    @fuel_monitor.add(self) 
  end
	
  def update
	update_fuel_display(@fuel_monitor.fuel_level)
  end

  def update_fuel_display
    ...
  end
end

The rocket ship is really fancy, not only does it fly us through space, it also has disco lights [because…why not!], and other non-essential things that should be turned off when we are low on fuel.

class DiscoLight
  def initialize(fuel_monitor)
    @fuel_monitor = fuel_monitor
    @fuel_monitor.add(self) 
  end

  def update
	if @fuel_monitor.low_fuel?
	  turn_off_lights
    end
  end

  def turn_off_lights
	...
  end
end

This was definitely a contrived example, but you can get an idea of how this pattern works. When you make a new observer, like DiscoLight or DashboardDisplay, you pass it the FuelMonitor that it should observe: DiscoLight.new(fuel_monitor). When the fuel monitor, the subject, has a state change, calling notify() will alert all of its observers and call each of their update() methods. Each observer’s update() then has its own independent responsibilities to carry out.

In the rocket ship above, we’re using the pull implementation where the observers are responsible for retrieving the data they need from the subject. This allows all the logic that is related to the subject to say inside the subject object. However, if we used the push method instead, the FuelMonitor.notify() would look like this:

class FuelMonitor
	...
  def notify
    @observers.each do |observer|
	  observer.update(fuel_data)
    end
  end

  def fuel_data
	{ 
	  fuel_level: fuel_level,
	  fuel_contamination: fuel_contamination,
      # other fuel properties, etc.
	}
  end
  ...
end


and each observer’s update() method now takes fuel_data so the observer will need to DiscoLight.update() might have to look something like this:

def update(fuel_data)
  fuel_level = fuel_data[:fuel_level]
  
  if fuel_level < 5 
	turn_off_lights
  end
end

Now there is a crossover of responsibilities and the DiscoLight needs to know how to calculate if there is enough fuel. Ideally, DiscoLight should not need to know such details about another object. The logic related to the fuel should be kept in the FuelMonitor class, like the pull implementation allows us to do.

Speaking of maintaining logic…

Encapsulation

Observer pattern is a great way of encapsulating all the data and business logic of one thing into a subject object. This allows for multiple objects that may rely on the the subject’s data to be encapsulated into observer objects. While the observers may communicate with the subject, the subject is completely independent from the observers.

If you’re saying to yourself, “hey, this looks awfully similar to event listeners”, good eye! Here’s an explanation on Observer Pattern vs. Event Listeners.

Hope this helps you see the usefulness of the Observer Pattern! If you’re interested in more design patterns, check out our other posts on Object-oriented Programming and OOP design patterns:

Leave a Reply