Beej's Bit Bucket

 ⚡ Tech and Programming Fun

2010-01-07

The Observer Pattern

Friendly Goat Photo 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 Observers. Then we'll add those Observers 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.

Comments

Blog  ⚡  Email beej@beej.us  ⚡  Home page