2010-01-07
The Observer Pattern
Right now, the GoatDetector is notifying you: "Goats detected!"
The idea of Design Patterns in computer science is one of many ideas that hadn't be formalized when I was first learning about computers, but that's not going to stop me from talking about it, now is it? 😀
The basic idea is that there are a number of recurring problems that occur in a variety of projects, and there are "design patterns" that can help solve these problems in a fairly well-defined way. They aren't algorithms, exactly; they're more like software design structures that help data flow in useful ways.
For instance, let's say you have a class and you want to instantiate a single instance of that class that everyone else can get a reference to and use. There's a pattern for that (The Singleton Pattern), but today I'm talking about another one: The Observer Pattern.
This pattern is common enough that there are libraries for it distributed with Java, and it's fairly integral to workflow in ActionScript. And there are libraries for it in just about every language. But here we'll do a quick-and-dirty Python implementation for demonstration purposes.
The basic idea is that you have an object that is "observed" by other objects, and when some event happens to the "observable" object, all the observing objects "see" it happen. Often this in implemented by having the observable object notify all its observers that something interesting has happened.
A couple places where this is used is for asynchronous events like receiving network data, or getting a GUI event (like a button press). For instance, in ActionScript if an object wants to receive notification that a particular button has been hit, it can call that button's
addEventListener()
method, passing a callback function:okButton.addEventListener(MouseEvent.CLICK, this.okButtonWasPressed);
then when the
okButton
is hit, it notifies all its observers by calling all the previously registered callbacks:private function okButtonWasPressed(event:Event):void { trace("The OK button was pressed!"); }
One of the great benefits of this system is that the observer can handle multiple "listeners", so that an arbitrary number of other objects can register their interest in a particular event occurring; it's a good general solution that requires no further code changes on the observer if later in the implementation process other subsystems become interested in that observer's events. If that happens, that subsystem can just register itself as a listener and respond to the event.
A not-quite-contrived example might be this. Let's say you have implemented a game, and it's all working quite well when the Higher Powers ask if you can add a little graphic on the sidebar that changes from a smiley face into a frowny face when the player dies. If the player object is an observable that is notifying observers when it dies, you can just add the smiley/frowny face as another observer to the player's notification list so the face can update itself when it is notified of the player's death—no need to actually modify the player code.
First, we'll make an Observable
class, which we'll subclass later.
This class should be able to keep a list of observers who are interested
in its events, and should have a way to notify all those observers that
an event has occurred. We'll do this by having it call all of the
observers' observeEvent()
methods:
class Observable(list):
def addObserver(self, observer):
self.append(observer)
def notifyObservers(self, event):
for observer in self:
observer.observeEvent(event)
Since it's keeping a list of observers, I just went ahead and subclassed
it from the list
built-in (which is an
"array", to you
non-Pythoners.) Because Observer
is-a list
, you can call all
normal list
methods on self
, like
append()
.
Then we're going to subclass Observable
to actually do something with
it. We're going to make NASA proud and write a routine that detects if
the word "goat" appears in a string, and then notifies all the observers
that it has occurred:
class GoatDetector(Observable):
def detect(self, line):
if line.find('goat') != -1:
self.notifyObservers('goats detected')
So when you call GoatDetector
's detect()
method with a string, it
looks to see if "goat" appears within it. And if it does, it notifies
all its observers of the happy occasion!
Now, earlier we saw that when we call notifyObservers()
, it will call
the observeEvent()
method on each observer. So all the observer
objects should definitely have that method defined! To facilitate this
for the demo, we'll go ahead and make an Observer class that does what
we want.
class Observer(object):
def __init__(self, name):
self.name = name
def observeEvent(self, event):
sys.stdout.write('=== %s: "%s"\n' % (self.name, event))
When an Observer
's observeEvent()
method is called, it prints out
its name (which was passed in the Observer
's constructor), and the
event that has come in. This is all for demo purposes when we run the
program.
Now for the main code, we'll construct a GoatDetector
and a couple
Observer
s. Then we'll add those Observer
s to the GoatDetector
instance so that when it detects goats, it'll notify them:
goatD = GoatDetector()
alice = Observer('Alice')
bert = Observer('Bert')
goatD.addObserver(alice)
goatD.addObserver(bert)
Now we're set to go! All we need to do is call goatD.detect()
with a
string, and when it detects goats, alice
and bert
will be notified.
Let's feed it from
stdin
for testing:
for line in iter(sys.stdin.readline, ""): # iter/readline for line-buffering
goatD.detect(line)
And now to give it a run (anything not beginning with ===
is what we typed):
There are all kinds of animals on the farm.
There are horses.
There are sheep.
There are goats.
=== Alice: "goats detected"
=== Bert: "goats detected"
There are cows.
There are pigs.
There are goats again.
=== Alice: "goats detected"
=== Bert: "goats detected"
There are puppies.
And that's the basics of the thing! Of course, there are all kinds of details and variations of implementation. Remember that The Observer Pattern, like all patterns, isn't a single specific strict implementation; it's a concept that can be implemented in places where it makes sense.
Here's the full Python implementation of the demo—it includes a bonus duck detector! Holy Moly! And here you thought I didn't get you anything for Christmas:
#!/usr/bin/python
import sys
# ----------------------------------------------------------------
class Observable(list):
"""
A class of thing that can be observed. When its notifyObservers()
method is called with an event, it passes that event on to its
observers.
"""
def addObserver(self, observer):
self.append(observer)
def notifyObservers(self, event):
for observer in self:
observer.observeEvent(event)
# ----------------------------------------------------------------
class Observer(object):
"""
A class of thing that observes an Observable. The Observable will
calls the Observer's observeEvent() method.
"""
def __init__(self, name):
self.name = name
def observeEvent(self, event):
sys.stdout.write('=== %s: "%s"\n' % (self.name, event))
# ----------------------------------------------------------------
class GoatDetector(Observable):
"""
This class notifies all its observers if a string contains the word
'goat'.
"""
def detect(self, line):
if line.find('goat') != -1:
self.notifyObservers('goats detected')
# ----------------------------------------------------------------
class DuckDetector(Observable):
"""
This class notifies all its observers if a string contains the word
'duck'.
"""
def detect(self, line):
if line.find('duck') != -1:
self.notifyObservers('ducks detected')
# MAIN -----------------------------------------------------------
goatD = GoatDetector()
duckD = DuckDetector()
alice = Observer('Alice')
bert = Observer('Bert')
chris = Observer('Chris')
goatD.addObserver(alice)
goatD.addObserver(bert)
duckD.addObserver(chris)
for line in iter(sys.stdin.readline, ""): # iter/readline for line-buffering
goatD.detect(line)
duckD.detect(line)
Now, I talked with my good pal Bastich about this blog entry, and he
suggested I update it to demonstrate an Observable
that would notify
its observers of different types of events, which, in real life, is more
useful than one that just handles a single event. I had originally
written it to just respond to a single event in the interest of clarity,
but responding to multiple kinds of events is more common. To that end,
I include here a different version of the code that allows this
behavior.
In this new code, the observer tells the AnimalDetector
what kind of
animals it's interested in hearing about. The AnimalDetector
is
equipped to detect a variety of animals, unlike the previous example's
GoatDetector
and DuckDetector
, which would only respond to a single
event each. (The technical difference is that instead of just storing a
list of observers, we store a dict
(that's "Associative
Array", to
non-Pythoners) of events and each dict
entry is a list of observers.
This way the Observable
can remember, "For event X, we have a list
of listeners L.")
So here's that code:
#!/usr/bin/python
import sys
# ----------------------------------------------------------------
class Observable(dict):
"""
A class of thing that can be observed. When its notifyObservers()
method is called with an event, it passes that event on to its
observers.
"""
def addObserver(self, event, observer):
if self.has_key(event):
self[event].append(observer)
else:
self[event] = [observer]
def notifyObservers(self, event):
if self.has_key(event):
for observer in self[event]:
observer.observeEvent(event)
# ----------------------------------------------------------------
class Observer(object):
"""
A class of thing that observes an Observable. The Observable will
calls the Observer's observeEvent() method.
"""
def __init__(self, name):
self.name = name
def observeEvent(self, event):
sys.stdout.write('=== %s: "%s"\n' % (self.name, event))
# ----------------------------------------------------------------
class AnimalDetector(Observable):
"""
This class notifies all its observers if a string contains a
particular animal.
"""
def detect(self, line):
animals = ('goat', 'sheep', 'duck', 'horse', 'platypus')
for a in animals:
if line.find(a) != -1:
self.notifyObservers(a)
# MAIN -----------------------------------------------------------
animalD = AnimalDetector()
alice = Observer('Alice')
bert = Observer('Bert')
chris = Observer('Chris')
dave = Observer('Dave')
# Notice how we have multiple observers looking for the same event in
# the following code. Also notice how the 'cow' event will never trigger
# for bert, because the AnimalDetector doesn't dispatch an event for
# cows!
animalD.addObserver('goat', alice)
animalD.addObserver('cow', bert)
animalD.addObserver('goat', bert)
animalD.addObserver('goat', chris) # same as alice!
animalD.addObserver('platypus', chris)
animalD.addObserver('duck', dave)
animalD.addObserver('platypus', dave)
for line in iter(sys.stdin.readline, ""): # iter/readline for line-buffering
animalD.detect(line)
As an exercise for the reader, I'd suggest adding a RemoveObserver()
method to the Observable
class—an observer could use this method to
let the Observable
know that it's not interested in receiving
notifications about that particular event any longer.