Observer—Evolving into a pattern, Part 1

IN THIS ARTICLE, I will describe the Observer pattern; but that is a minor objective. The primary objective of this article is to give you a demonstration of how your design and code can evolve to use a pattern—or not.

In future columns, I will use many patterns. Often, they will be presented fait accompli, without showing how the code evolved to use the pattern. This might give you the idea that patterns are simply something you insert into your code and designs in completed form. This is not what I advise. Rather, I prefer to evolve the code I am working on in the direction of a pattern. I may get to the pattern, or I may not. It depends upon whether the issues are resolved. It is not uncommon for me to start with a pattern in mind, and wind up at a very different place.

This article sets up a simple problem, and then shows how the design and code evolves to solve that problem. The goal of the evolution is the Observer pattern. At each stage of the evolution, I will describe the issues I'm trying to resolve, and then show the steps that resolve them.

The Digital Clock
We have a clock object that catches millisecond interrupts (known as tics) from the OS and turns them into the time of day. This object knows how to calculate seconds from milliseconds, minutes from seconds, hours from minutes, days from hours, etc. It knows how many days are in a month, and how many months are in a year. It knows all about leap years. It knows about time (see Figure 1).

Figure 1
Figure 1. OS and Clock.

I'd like to create a digital clock that sits on the desktop and continuously displays the time of day. What is the simplest way to accomplish this? I could write:


public void DisplayTime
{
  while(1)
  {
     int sec = clock.getSeconds();
     int min = clock.getMinutes();
     int hour = clock.getHours();
     showTime(hour,min,sec);
  }
}
Clearly, this is suboptimal. It consumes all of the available CPU cycles to repeatedly display the time. Most of these displays will be wasted because the time will not have changed. It may be that this solution would be adequate in a digital watch or a digital wall clock, as conserving CPU cycles may not be very important in those systems. However, I don't want this CPU hog running on my desktop.

To fix this, I could simply add the following delay:


public void DisplayTime
{
  while(1)
  {
     int sec = clock.getSeconds();
     int min = clock.getMinutes();
     int hour = clock.getHours();
     showTime(hour,min,sec);
     wait(1000); // one second.
  }
}
For many applications this would be sufficient, and I could stop right here, never getting close to the Observer. But let us suppose the digital clock must never waste any CPU time, and must always display the time as accurately as possible—not even a second late. Clearly, no simple delayed loop will do.

To meet this constraint, the way in which the time moves from the clock to the display will be nontrivial. What mechanism should I use? But before I ask this question, I need to ask "How do I test that the mechanism is doing what I want?"

The fundamental problem is how to get data from the Clock to the DigitalClock. I will assume the Clock and DigitalClock both exist. My interest is in how to connect them. I can test that connection by making sure the data I get from the Clock is the same data I send to the DigitalClock.

A simple way to do this is to create one interface that pretends to be the Clock, and another that pretends to be the DigitalClock. I can then write special test objects that implement those interfaces and verify that the connection between them works as expected (see Figure 2).

Figure 2
Figure 2. Interfaces for testing the ClockDriver.

The ClockDriverTest object will connect the ClockDriver to the two mock objects through the TimeSource and TimeSink interfaces. It will then check each of the mock objects to ensure the ClockDriver manages to move the time from the source to the sink. If necessary, the ClockDriverTest will also ensure that efficiency is conserved.

I think it's interesting that we have added interfaces to the design simply as a result of considering how to test it. In order to test a module, you have to be able to isolate it from the other modules in the system, just as we have isolated the ClockDriver from the Clock and DigitalClock. Considering tests first helps us to minimize the coupling in our designs.

So how does the ClockDriver work? Clearly, in order to be efficient, the ClockDriver must detect when the time in the TimeSource object has changed. Then, and only then, should it move the time to the TimeSink object. How can the ClockDriver know when the time has changed? It could poll the TimeSource, but that simply recreates the CPU hog problem.

The simplest way for the ClockDriver to know when the time has changed is for the Clock object to tell it. We could pass the ClockDriver into the TimeSource and then have the TimeSource update the ClockDriver. The ClockDriver will, in turn, set the time on the ClockSink (see Figure 3).

Figure 3
Figure 3. Setting the source and updating the driver.

Notice the dependency from the TimeSource to the ClockDriver. I'm not very happy with this, as it implies that TimeSource objects must use ClockDriver objects in every case. However, I'll defer doing anything about this until I get this working.

Listing 1 shows the test case for the ClockDriver. Notice that it creates a ClockDriver and binds a MockTimeSource and a MockTimeSink to it. It then sets the time in the source, and expects the time to magically arrive at the sink. The rest of the code is shown in Listings 2 through Listing 6.

OK, now that it works, I can think about cleaning it up. I don't like the dependency from TimeSource to ClockDriver. I don't like it because I want the TimeSource interface to be usable by anybody, not just ClockDriver objects. As it stands, only ClockDriver instances can use a TimeSource. We can fix this by creating an interface that TimeSource can use, and that ClockDriver can implement. We'll call this interface ClockObserver. (See Figure 4, and Listing 7 through Listing 10. The code in bold type has changed.)

Figure 4
Figure 4. Adding the Clock Observer interface.

This is better. Now anybody can make use of TimeSource. All they have to do is implement ClockObserver, and call SetObserver passing themselves in as the argument.

I'd like to be able to have more than one TimeSink getting the time. One might implement a digital clock. Another might be used to supply the time to a reminder service. Still another might start my nightly backup. In short, I'd like a single TimeSource to be able to supply the time to multiple TimeSink objects.

How do I do this? Right now, I create a ClockDriver with a single TimeSource and a single TimeSink. How do you specify multiple TimeSink instances? I could change the constructor of the ClockDriver to take just the TimeSource and then add a method named AddTimeSink that allows me to add TimeSink instances any time I want.

The thing I don't like about this is that I now have two indirections. I have to tell the TimeSource who the ClockObserver is by calling setObserver, and I also have to tell the ClockDriver who the TimeSink instances are. Is this double indirection really necessary?

Looking at ClockObserver and TimeSink, I see they both have the setTime method. It looks like TimeSink could implement ClockObserver. If I did this, then my test program could create a MockTimeSink and call setObserver on the TimeSource. I could then get rid of the ClockDriver (and TimeSink) altogether. Listing 11 shows the changes to ClockDriverTest.

This means that MockTimeSink should implement ClockObserver rather than TimeSink (see Listing 12). These changes work fine. Why did we think we needed a ClockDriver in the first place? Figure 5 shows the UML.

Figure 5
Figure 5. Getting rid of ClockDriver.

Clearly, this is much simpler.

OK, now we can handle multiple TimeSink objects by changing the setObserver function to registerObserver, and making sure that all the registered ClockObserver instances are held in a list and updated appropriately. This requires another change to the test program. Listing 13 (please see online listing in our Code section) shows the changes. I also did a little refactoring of the test program to make it smaller and easier to read.

The change needed to make this work is pretty simple. We change MockTimeSource to hold all registered observers in a Vector. Then, when the time changes, we iterate through the vector and call update on all the registered ClockObservers. Listing 14 and Listing 15 (please see online listings in our Code section) show the changes. Figure 6 shows the corresponding UML.

Figure 6
Figure 6. Multiple Clock Observers.

Conclusion
This is pretty nice. We've gotten close to the Observer pattern. There are a few things I don't like about this that I will explore in my next column. For one thing, I don't like the fact that MockTimeSource has to deal with the registration and update. It implies that Clock, and every other derivative of TimeSource will have to duplicate that registration and update code. Exploring this will take us down an interesting rat hole.

This article comes from a chapter of Agile Software Development Principles, Patterns, and Process, by Robert C. Martin, Prentice Hall, 2001.

Featured

Upcoming Events

AppTrends

Sign up for our newsletter.

I agree to this site's Privacy Policy.