Observations on Observer

zopiloteMy first encounter with the Observer design pattern was circa 1998.  The GoF book was still new–published in 1994–and the community was still assimilating design patterns. Java  the Hot* even offered an Observer interface and an Observable class. What could be simpler?

However, I’ve used Observer exactly once.  The main reason is that Observer is a scaffolding pattern–it is good to understand the concepts, but once you try to implement a robust version of it, it becomes complicated very quickly.  Furthermore, you often graduate to a better version of it, and often times there are language-specific facilities or libraries that implement a variation on Observer. In essence, not worth your squeeze.

Consider this naïve implementation in C++:

struct observer {
  virtual void update() = 0;
};

class subject {
 set<observer> observers;
 public:
  void notify() { 
   for_each(begin(observers), end(observers), mem_fn(&observer::update));
  }

  add(const observer& o) { observers.insert(o); }
  remove(const observer& o) { observers.erase(o); } 
};

class concrete_subject : public subject {
 ...
};

The problems start very quickly.  They’ve been discussed ad naseam over the years. One of the most complete discussions I’ve read are by Alexandrescu [1], [2] and Sutter [3].

  1. What type of container is best? For example, can an observer register more than once? Can observers be notified in a specific order?
  2. Is the container thread-safe? For example, can observers be added or removed concurrently from different threads.
  3. What is the type a container stores?  Storing an observer requires it is copy-constructible, so perhaps storing an observer* is best.  However, if the observer goes out of scope without being removed from the subject one will end up with a dangling pointer.  How about using a shared_ptr<observer>?  If all but the reference stored in a subject are released, unwanted notifications could be coming in.
  4. Can observers call remove from within the update method?  This could invalidate the iterator in subject::notify.
  5. What happens if an exception is thrown from the a concrete observer’s update member function? You don’t pass Go, you don’t collect $200.

These questions are not specific to C++–the same problems need to be solved Java or C#, for that matter, even when using delegates**, which behind the scenes implement a variation on Observer: It provides a collection to store the targets, add/remove methods with operators +=, -=, and a notification mechanism that executes on the thread where the delegate is invoked.

Java’s  Observer interface and Observable are not perfect, either. If you read the fine print, you’ll notice that the notifications can come in different threads.  Ouch. This means that you better make sure your event handler is thread-safe.

Notifications per Method Ratio (N:M)

A taxonomy for Observer that can let you choose what to use is the ratio of notifications per method.  Assume N is the number of notifications from a specific Subject and M is the number of methods to handle the notification.  When choosing, you can look at the N:M ratio and see if it makes sense to you.

In Observer, the N:M ratio is many:1, since all notifications are funneled through the update method.

In the Delegation Event Model (DEM) design pattern, all the notifications are grouped into one interface, with one method per notification. The N:M ratio is 1:1.

// Methods in Java interfaces are public.  Ditto in C#
// Events are grouped in one 
public interface XxxListener {
  void event1(XxxEvent e);
  void event2(XxxEvent e); 
}

public class MyXxxListener implements XxxListener {
  public void event1(XxxEvent e) { ... }
  public void event2(XxxEvent e) { ... }
}

public class MySubject {
  public addXxxListener(XxxListener lis) { ... }
  public removeXxxListener(XxxListener lis) { ... }
}

In C#, using delegates is a language-specific version of Observer,  where each delegate, while it can address several targets, it will support a notification per delegate, so the N:M ratio is also 1:1.

If you want notifications à la carte, in C++ you can make use of Boost.Signals2, a signals and slots, thread-safe library where you can get a notification per signal. You can consider signals a C++ alternative to delegates in C#. Added flexibility is that you can specify a priority for each slot. Just like in C#, there will be a signal per slot (or a notification per method). The N:M ratio is also 1:1.

In essence, when shopping around for Observer, beware of the consequences and trade-offs of the implementation, whether it is a language-specific solution, a library or a roll-your own implementation.

References

[1] Alexandrescu, Andrei,  Generic<Programming>: Prying Eyes: A Policy-Based Observer (I), C/C++ Users JournalApril 2005.

[2] Alexandrescu, Andrei,  Generic<Programming>: Prying Eyes: A Policy-Based Observer (II), C/C++ Users Journal, June 2005.

[3] Sutter, Herb,  Generalizing Observer, C/C++ Users JournalSeptember 2003.

* Could not resist paying homage to Star Wars character Jabba the Hutt.

** As an anecdote, when learning about delegates in the first-ever Guerrilla .NET at DevelopMentor before the .NET framework was released, I asked the instructor, Brent Rector, what happened if the object receiving the delegate call threw an exception.  He tried it, and the program crashed.

This entry was posted in design patterns, software. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s