Skip to content

Imperative Event Handling: The Observer Pattern

Rohit edited this page Jan 11, 2017 · 4 revisions

The traditional way to deal with user interfaces to handle the events there is based on what we call The Observer Pattern. We will discuss its advantages and shortcomings.

The Observer Pattern

The Observer Pattern is widely used when views need to react to changes in some model (which captures the state of the system). Some variants are also called

  • publish/subscribe
  • model/view/(controller).
 View1   subscribe     _______
 View2  ------------> |       | 
 View3                | Model |
 View4  <------------ |       |
 View5    publish     |_______|

The views subscribe to the Model and whenever there is a change, the Model publishes to the views and thus the views can change themself.

In MVC, the Controller manages the interactions between the Model and the View.

Implementation in Code

trait Publisher {
	private var subscribers: Set[Subscriber] = Set() // empty set
	def subscribe(subscriber: Subscriber): Unit = subscribers += subscriber
	def unsubscribe(subscriber: Subscriber): Unit = subscribers -= subscriber
	def publish(): Unit = subscribers.foreach(_.handler(this)) // invokes the handler function on each subscriber with the instance of the publisher as the argument
}

trait Subscriber {
	def handler(pub: Publisher)
}

Example: Observing Bank Accounts

We have seen the Bank Accounts Example previously, where the BankAccount changes state based on the balance in the account, which can change upon withdraw or deposit.

class BankAccount {
	private var balance = 0
	def deposit(amount: Int): Unit =
	if (amount > 0) {
		balance = balance + amount
	}
	def withdraw(amount: Int): Unit =
	if (0 < amount && amount <= balance) {
		balance = balance - amount
	} else throw new Error(”insufficient funds”)
}

We make this BankAccount a publisher by making it extend our trait Publisher, and then call the publish() method wherever its state changes:

class BankAccount extends Publisher {
	private var balance = 0
	def currentBalance: Int = balance // <----------------------we add an getter/accessor to the access balance. We don't make balance public as then it would be writable from outside.
	def deposit(amount: Int): Unit =
	if (amount > 0) {
		balance = balance + amount
		publish() // <----------------------
	}
	def withdraw(amount: Int): Unit =
	if (0 < amount && amount <= balance) {
		balance = balance - amount
		publish() // <----------------------
	} else throw new Error(”insufficient funds”)
}

Here is a Subscriber that "observes" all the BankAccounts and also maintains the total balance of a list of accounts:

class Consolidator(observed: List[BankAccount]) extends Subscriber {
	observed.forEach(_.subscribe(this))
	private var total: Int = sum()
	private def sum() = observed.map(_.currentBalance).sum
	def handler(pub: Publisher) = sum()
	def totalBalance = total
}

The Good

  • Decouples views from state
  • Allows to have a varying number of views of a given state
  • Simple to set up

The Bad

  • Forces imperative style, since handlers are Unit-typed
  • Many moving parts that need to be co-ordinated
  • Concurrency makes things more complicated
  • Views are still tightly bound to one state; view update happens immediately

To quantify (Adobe presentation from 2008):

  • 1/3rd of the code in Adobe’s desktop applications is devoted to event handling.
  • 1/2 of the bugs are found in this code

How to Improve?

The rest of this course will explore different ways in which we can improve on the imperative view of reactive programming embodied in the observer pattern..

  • This week: Functional Reactive Programming.
  • Next two weeks: Abstracting over events and eventstreams with Futures and Observables.
  • Last three weeks: Handling concurrency with Actors.