ReefPointsBlog

Design Patterns: The Observer Pattern

Doug Yun

Note: We won't be going over the Ruby module Observable. Instead, we'll building out the pattern ourselves.

Your First Day at the NSA

Welcome to the National Security Agency, Agent Smith. You have quite an impressive background, and we believe your "go-getter" attitude will instill a new kind of vigor within the organization.

Your cubicle is down to the left... here are some NDAs for you to fill out. I'll swing by your desk in the afternoon and pick them up from you later. Oh, and before I forget, here is your first assignment.

Go get 'em, tiger!

The First Assignment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Agent Smith
Spook First Class
[REDACTED]
NSA                                                     08-20-[REDACTED]

                     Operation [REDACTED] Observers

Welcome, Agent Smith:

Bluntly, we'd like to track everyone's emails.

Attached are two documents.

The first document will show you the basic structure of a typical email,
and the second document will provide you a basic profile of a suspicious
person.

If there are any questions, please reach me at [REDACTED].

Best of luck,





Agent [REDACTED]
[REDACTED]
[REDACTED]
NSA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Document 1:
# Basic structure of an email

module Email
  extend self

  def send(subject, sender, receiver)
    puts %Q[
      Subject: #{subject}
      From:    #{sender}@example.com
      To:      #{receiver}@example.com
      Date:    #{Time.now.asctime}
    ]
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Document 2:
# Characteristics of a suspicious person

class Person
  include Email
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def send_email(subject, receiver)
    Email.send(subject, name, receiver)
  end
end

As we look through the Email module, we see that it contains Email.send which takes three arguments: subject, sender, and receiver.

Gazing at the suspicious Person class, we see that it includes the Email module. Person#send_email takes two parameters: a subject and a receiver. Person#name will stand in as the sender of the email.

Hypothetically, let's see how a suspicious person would send an email:

1
2
3
4
5
6
7
bill = Person.new 'Bill'
bill.send_email 'Fishing Trip', 'Fred'
  # =>
      Subject: Fishing Trip
      From:    Bill@example.com
      To:      Fred@example.com
      Date:    Wed Aug 16 20:35:09 2006

Hmm... as you sit in your cubicle, you ponder the numerous possible ways of tracking emails. You won't need anything too complicated, just something to kick off a notification once an email has been sent.

Volia! You realize you can use the Observer pattern!

The Subject and its Observers

First, let's start off by creating two observer classes, Alert and Agent classes.

1
2
3
4
5
6
7
8
9
10
11
class Alert
  def gotcha(person)
    puts "!!! ALERT: #{person.name.upcase} SENT AN EMAIL !!!"
  end
end

class Agent
  def gotcha(person)
    puts "!!! TIME TO DETAIN #{person.name.upcase} !!!"
  end
end

Next, let's create a Subject module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module Subject
  attr_reader :observers

  def initialize
    @observers = []
  end

  def add_observer(*observers)
    observers.each { |observer| @observers << observer }
  end

  def delete_observer(*observers)
    observers.each { |observer| @observers.delete(observer) }
  end

  private

  def notify_observers
    observers.each { |observer| observer.gotcha(self) }
  end
end

Here within the Subject#initialize, we create an empty array which will contain a list of observers. Subject#add_observer simply pushes our desired observers into the array.

Finally, we can alter the suspicious Person class, which will act as the subject class. Let's include the Subject module now.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
  include Email, Subject
  attr_reader :name

  def initialize(name)
    # 'super' requires a parentheses because we're calling
    # super on the superclass, 'Subject'
    super()
    @name = name
  end

  def send_email(subject, receiver)
    Email.send(subject, name, receiver)
    notify_observers
  end
end

Subject#notify_observers calls #gotcha on each observer, which informs each observer that Person#send_email has been kicked off.

Now let's give it a whirl...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
alert = Alert.new
agent = Agent.new

bill = Person.new 'Bill'

bill.add_observer alert, agent   # Bill now has two observers watching him

bill.send_email 'Fishing Trip', 'Fred'
  # =>
      Subject: Fishing Trip
      From:    Bill@example.com
      To:      Fred@example.com
      Date:    Wed Aug 16 20:35:09 2006

!!! ALERT: BILL SENT AN EMAIL !!!
!!! TIME TO DETAIN BILL !!!

Perfect, it works! Now we can start protecting our freedom!

Discussion

In our example above, we have two observers, the Alert and Agent classes, and a subject, Person. By creating the Subject module, any instance of Person now informs and updates any observer through #notify_observers, ultimately removing any implicit coupling from Alert and Agent.

There are a few similarities between the Observer and Strategy patterns. Both patterns employ an object (the Observer's subject and the Strategy's context) that makes calls to another object (the Observer's observer or Strategy's strategy). The difference between the two patterns is the purpose and use case. The Strategy pattern relies on the strategy to do the work, while the Observer pattern informs the observers of what is going on with the subject.

Hope you enjoyed this short example, thanks for reading!