Design Pattern #2: Observer Pattern
Overview
observer pattern = publisher + subscriber
Pattern definition:
The observer pattern defines a one-to-many dependency between objects so that when one object changes stats, all of its dependents are notified and updated automatically.
Starting example: weather monitoring application
class WeatherStation { def measurementsChanged():Unit = { val temp = getTemperature val humidity = getHumidity val pressure = getPressure // we want to make the displays extensible (i.e. decouple them), also maybe subscribe/unsubscribe during runtime currentConditionsDisplay.update(temp, humidity, pressure) statisticsDisplay.update(temp, humidity, pressure) forecastDisplay.update(temp, humidity, pressure) } }
Solution round 1: bad data encapsulation
Note the Double temperature is passed everywhere!
trait DisplayElement { def display:Unit } trait Observer{ def update(temp:Double):Unit } trait Subject { val observers:ListBuffer[Observer] = ListBuffer[Observer]() def registerObserver(observer: Observer) = observers += observer def removeObserver(observer: Observer) = observers.remove(observers.indexOf(observer)) def notifyObservers(temp:Double):Unit } class WeatherStation extends Subject{ var temperature:Double = 0.0 def notifyObservers(temp:Double) = observers.foreach(_.update(temp)) def setMeasurement(temp:Double) = { temperature = temp notifyObservers(temp) } } class CurrentConditionDisplay(subject: Subject) extends Observer with DisplayElement{ // we're passing the subject in constructor so that we can call its register methods and other methods for pull subject.registerObserver(this) var temperature:Double = 0.0 def display = println(s"current condition display, temperature: $temperature") def update(temp:Double) = { temperature = temp display } // NOTE: if using the pull model, would be something like this: def update = { temperature = subject.asInstanceOf[WeatherStation].temperature display } } object Main { def main(args:Array[String]):Unit = { val station = new WeatherStation val display = new CurrentConditionDisplay(station) station.setMeasurement(10.0) } }
Solution round 2: better data encapsulation
The java.util.Observer
use Object
type to pass data around: update(Observable o, Object arg)
All the others the same, but we encapsulate data:
Note: scala's trait proves superior again as you can have concrete implementation but still can compose (as opposed to in Java, it uses a class for Observable
, thus limiting its reuse potential)
// DisplayElement and Main stays the same trait Data trait Observer{ def update(subject: Subject, data: Data):Unit // note we pass both subject (so that observers can also use pull model) and data here } trait Subject { val observers:ListBuffer[Observer] = ListBuffer[Observer]() def registerObserver(observer: Observer) = observers += observer def removeObserver(observer: Observer) = observers.remove(observers.indexOf(observer)) def notifyObservers(data: Data):Unit // we pass data to all observers } case class WeatherData (temperature:Double) extends Data class WeatherStation extends Subject{ var temperature:Double = 0.0 def notifyObservers(data: Data) = observers.foreach(_.update(this, data)) def setMeasurement(temp:Double) = { temperature = temp notifyObservers(WeatherData(temp)) } } class CurrentConditionDisplay(subject: Subject) extends Observer with DisplayElement{ subject.registerObserver(this) var temperature:Double = 0.0 def display = println(s"current condition display, temperature: $temperature") def update(subject: Subject, data: Data) = { temperature = data.asInstanceOf[WeatherData].temperature // we need to cast to WeatherData display } }
Design Principle
Strive for loosely coupled designs between objects that interact.
Others
-
push
andpull
style -
java.util.Observer
also has asetChanged()
method to control notifications. - Never depend on a specific notification order.
Comments
Comments powered by Disqus