Virtual Developer Workshop: Containerized Development with Docker
Many hands make work light, likewise many objects join hands in the development of complex systems. The rule of thumb in Object Oriented Design is to decompose the system into a set of cooperating objects. Each decomposed object must be complete and it should express a high degree of cohesion and have a low degree of coupling with other objects. High degree of cohesiveness means that the object is self-contained and low degree of coupling means it is not very dependent on other objects. However, in the world of objects, each object should interact with the fellow objects to provide the complete solution. This article explains how to avoid object dependencies using the Observer Pattern with a simple example. It also talks about when and why the Observer Pattern is needed, its benefits and liabilities.
Design Patterns are meant to provide a common vocabulary for communicating design principles. The Observer Pattern is classified under Object Behavioral Patterns in the book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (Addison-Wesley, 1995). In this article, I will be using the terms used by ' Gang of Four (GoF) ' to explain Observer Pattern. First, let us understand the problem of object interaction.
An object has an internal state and provides a set of services. Usually, services are implemented as methods and data members determine the state of the object. Let us take an example of interaction between two objects. Object 'A' needs to maintain a reference to Object 'B' directly or indirectly to use its services. In languages like C++, Object 'A' should include the header file containing the class definition of Object 'B'. This is fine when the classes of Objects 'A' and 'B' belong to the same abstraction, so that they are always reused together. However, when the classes are unrelated, they are totally independent and can be used in two different contexts in two different applications. Including 'B's' header file in Object 'A' makes it impossible to reuse 'A' without 'B'.
Sometimes, the state of object 'A' may depend on the state of object 'B'. Whenever 'B' changes, 'A' should recompute its state to remain in Sync with 'B'. The situation becomes more complicated when a new Object 'C', which is dependent on the state of 'B' is brought into the application. In short, the dependency and the coupling between the objects are increased and the reusability is reduced. How can we make two or more independent objects work together without knowing each other's existence? The answer is The Observer Pattern.
The Observer Pattern
GoF classifies the Observer Pattern as an Object Behavioral Pattern. The Observer Pattern is intended to "Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically". An object that is subjected to change is called a Subject and an object that depends on the Subject's state is called an Observer. In the above example, 'B' can be a Subject and Objects A and C can be Observers. A Subject can have any number of Observers. When the Subject changes its state, it Notifies all the Observers and the Observers query the Subject to maintain state consistency with the Subject.
UML Class Diagram and Participants
Participants of the Observer Pattern is shown in the UML class diagram. Subject maintains a list of Observers and provides an interface for attaching and detaching them. Observer defines an abstract Update interface, which is called by the Subject when its state changes. Concrete Subject maintains state of interest for Concrete Observers and notifies them when the state changes. Concrete Observer implements the update interface provided by the Observer. It also maintains a reference to Concrete Subject and updates its state to be in sync with the Concrete Subject.
This article uses Stair-Master as an example to explain the concept of the Observer Pattern. Stair-Master is a fitness equipment mainly intended for Cardiovascular exercises. It simulates the effect of climbing stairs and offers different kinds of fitness programs including Fatburn, Cardio, Random and Hill. The person using the Stair-Master must specify the workout duration, age, effort level and the workout program. It contains a set of monitors that are used to control and visually indicate the progress of the workout. Time monitor keeps track of the elapsed workout time and is responsible for stopping the workout after the specified duration. Heart rate monitor reads the current heart rate and indicates it with respect to low and high values for the current workout program. Program controller can be used to change the workout program and effort level at any time during the workout. Calorie monitor displays total calories burned and average caloric burn per minute.
Target heart rate for a person is calculated based on the age and the workout program selected. Normally, Fatburn programs have lower heart rates when compared with Cardio programs. Time monitor interacts with the Heart rate monitor to read heart rate every second and with the Calorie monitor to update the total calories burnt. When the workout program changes, the Program controller informs the Heart rate monitor to recompute the target heart rate.
Time monitor, Heart rate monitor, Program controller and Calorie monitor can be modelled as independent objects, so that they can be used in the design of any Cardio equipment. Some Stair-Masters can have Distance monitors to show the total floors climbed and the total distance covered in meters. Also, advanced machines can use a Heart rate variation monitor instead of a Heart rate monitor to indicate the variation in the heart rate over a period of time.
Direct Object Interaction
First, let us see what the problems are in having direct interactions between independent objects. If the objects are directly interacting with each other, they have to hold a pointer or the reference to the dependent object. This makes it difficult toi) Add a new dependent object
ii) Replace an existing dependent object with a new object
iii) Remove an existing dependent object.
i) Adding a new dependent object
In the next version, if the Distance monitor is added to the Stair-Master, the dependency is further increased and the Time monitor should be changed to maintain a reference to the Distance monitor. The refined dependency graph is shown below
ii) Replacing an existing dependent object with a new object
When a Heart rate variation monitor replaces the Heart rate monitor, the Time monitor and the Program controller needs to be changed (See above diagram for object dependencies).
iii) Removing an existing dependent object
It is not necessary for a Cardiovascular equipment to have all the above mentioned monitors. Some Elliptical cross trainers don't have a Heart rate monitor. Since Time monitor and Program controller maintains direct references to Heart rate monitor, they cannot work without Heart rate monitor. This makes it impossible to remove Heart rate monitor, even if it is not needed.
Using Observer Pattern
Observer Pattern can be used to address all the above mentioned problems. Time monitor, Heart rate monitor, Calorie monitor and Program controller can be treated as Observers and a new object Cardio Subject can be introduced to act as a Subject. These monitors will not know the existence of each other and they always communicate through the Cardio Subject. When one of the monitors changes its state (say Time Monitor), it updates the Cardio Subject, which in turn notifies all the monitors. In response to the notification, the monitors query the Cardio Subject to update their states. Since the monitors are independent of each other, a new monitor can be easily added, an existing monitor can be removed or replaced with a new one. Observer Pattern is demonstrated using a complete Visual C++ Stair-Master application.
Change propagation mechanisms
When the state of the Subject is changed, the Observers must have their states changed. This section contains two change propagation mechanisms, Notification and Polling, which can be used to maintain state consistency between the Subject and Observers.
When there is change in Subjects state, its Observers are notified by calling the Notify method. In response to the notification, the Observers query the required data from the Subject to maintain state consistency. Observers can even register for specific events of interest and in the occurrence of such an event, the Subject can send notifications only to the interested Observers.
Maintaining self-consistency of the Subject and avoiding redundant notifications are two important issues that should be considered in the Notification process.
Maintaining self-consistency of the Subject
Self-consistency is all about retaining state of the Subject after notifying the Observers. It is important to make sure that the Subjects state is self-consistent before and after calling Notify method to keep the state of Observers in sync with the Subject. It is difficult to maintain self-consistency in inherited operations, that too, when Notify method is called in the base class. A simple example is shown in example of self consistency violation in the Subject is shown in Listing 1.
In this example, CDervSubject overrides the operation Calculate and calls the base class method, which sends the notification to all the Observers. The derived class implementation can change the value of the data member m_nResult, bringing in the problem of self-inconsistency. To avoid this, GoF suggests the use of Template methods. Primitive operations can be defined as protected virtual methods that can be overridden by the derived classes and call the Notify method as the last operation in the template method to maintain self-consistency. Listing 2 illustrates using template method to maintaining self-consistency of the Subject illustrates this.
Avoiding redundant notifications
Sometimes, calling Notify method in each and every state changing method is not necessary. A single Notify call after all the state changes will avoid redundant notification calls. In Listing 3, the Observer receives three notification calls instead of one, the Observer receives three notification calls instead of one. This can be achieved by using a Change Tracker class that has a set of Change Tracking methods (see Listing 4). To ensure a single notification call at the end of all state changes, CMySubject should inherit from CChangeTracker and implement the methods. Instead of calling Notify method in all state changing methods, CMySubject calls StartChange before making the change and FinishChange after making the change. A change reference count is incremented during StartChange and decremented during FinishChange and Notify method is called only when the reference count reaches 0. The advantage of this approach is any number of state changing methods can be called in any sequence and the Observers will be notified only once at the end of all the state changes. Listing 4 for an example of notification using Change Tracker illustrates this.
- Simple and straightforward implementation in most cases.
- Change in the state of the Subject is immediately propagated to all the Observers.
- Subject can be reused only with the Observer abstraction. This reduces the reusability of the Subject in a totally different context.
- Subject should maintain a list of Observers. In some cases, the Observers may request only for specific events for which the Subject has to notify them. Subject should also maintain this information along with the list of Observers, which increases the overhead.
- Since Observers are independent of each other, changing Subjects state in the Update method of an Observer without a well-defined dependency criterion should not be encouraged. Doing so may result in
ii) Inconsistent Observer states, that is each Observer will be in different states at the same time.
Polling the subject for state changes
In this approach, the Observer polls the Subject for state changes. When the Subject changes, the Observer is not notified with the change, but the Observer uses a polling algorithm to read the state change from the Subject. Polling is widely used in popular class libraries. MFC uses the polling technique to update user-interface objects. See Polling in MFC for more details.
- The Subject is totally unaware of Observers. It need not maintain the list of Observers and notify them about the state changes.
- Observers take the responsibility of maintaining the state consistency. This increases the reusability of the Subject.
- When to poll the Subject. Subject should be polled at the right time, if the Subject is polled too early, the Observers will have old state and if the poll is too late, they lose the intermittent states.
- Performance overhead is introduced when polling a Subject, which has not changed.
- Somebody should ask the Observers to poll the Subject. Who notifies the Observer do this can be very big question?
Object Interaction Models
The Subject and the Observers can interact using a Push Model or a Pull Model.
In this model, the Subject pushes the state changes to the Observers. Push model can be used when all the Observers are interested in common state changes. Observers have no choice other than receiving the pushed data. Push model cannot be used with large amount of data, as an Observer may not be interested in all the data that is being pushed. Also, the Subject must know something about the Observer to which it is pushing the data. Therefore, the Observers have to comply with a standard interface required by the Subject and the Subjects reusability is limited to these Observers.
In this model, the Subject notifies the state change to the Observers and the Observers pull only the required data from the Subject. Pull model is more flexible and the Observers can pull only the required data. But, more than one method call is required to implement the pull model. The first method call is for change notification from the Subject to all its Observers and an interested Observer must call at least one more method to pull the data. In a very dynamic environment, the state of the Subject can change between these two calls, that is before the Observer pulls the data. Therefore, the Subject and the Observers may not be in sync. Above all, the Observers call specific methods to pull the required data and it is up to them to figure out what is changed without getting much help from the Subject. Sample application presented with this article uses the Pull Model.
- The Observer Pattern avoids direct object interactions and can be used when one or more objects are interested in the state changes of a given object.
- It can be used to develop loosely coupled applications maintaining object state dependencies.
- It can be used when the number of state dependent objects is not known in advance or even when the number may even change over time.
- Subject and the Observer objects can be reused separately and they can vary independently.
- The Observer Pattern can be used to develop application in layers. For example, a user interface application can contain spreadsheet, graph, and chart objects (Observers, at a higher level) that are dependent on the data from a database object (Subject, at a lower level). When the data in the database object changes all the Observers are automatically notified through the abstract operation defined in the Observer base class. Since the abstract operation is defined in the base class, the Subject need not know the concrete Observer classes and hence they are loosely coupled.
- Since all the Observers are notified through the same abstract operation, it is easy to add or remove Observers on demand.
- In most situations, Observers will simply be only notified about a state change. It is up to the Observer to find out what exactly has changed. However, this can be avoided by including additional information (or the Aspect of change) along with the notification. This will give some clue about the change to the Observer.
- Observer objects are totally independent and they have no knowledge on the existence of the fellow Observers. Therefore, an Observer object can change the state of the Subject (in the Update method) before even all the Observers are notified. This may result in state inconsistencies and the state change notifications will be lost.
- Whenever an Observer changes the Subjects state, all the dependent Observers are notified. If the dependency criteria is not well defined, recursive updates can easily happen. Usually, in Stair-Masters the workout program and the heart rate are mutually dependent. That is, when the program changes (say from Fat Burn to Cardio) the heart rate changes and when the heart rate range falls below or goes above a threshold limit the program automatically changes. For the sake of simplicity, this dependency criterion is not enforced in the example. Assuming that the dependency criterion is not well defined in the example, Program controller will update the Cardio Subject when the program changes and Heart rate monitor will recompute the heart rate. If the heart rate falls below or goes above the threshold level, the heart rate monitor will update the Cardio Subject to change the program, which in turn will update the Program controller and this cycle repeats. Therefore, Heart rate monitor and Program controller blindly updates the Cardio Subject, which notifies both these monitors to update themselves resulting in recursive update calls.
- Observer Pattern introduces an additional level of indirection to maintain state consistency. This increases the flexibility in the application design, but has a sure performance impact. Also, too many indirections decrease the understandability of the code.
- When the Subject is deleted, the Observers dont have a direct way to know about the deletion. Therefore, the Observers will have dangling reference to the deleted Subjects.
This section presents known uses of the Observer Pattern. Some of the known uses presented in this section are taken from the GoF book on Design Patterns.
Observers should poll the Subject at the right time to get its state. In a MFC application, the user-interface objects such as menu items and toolbar buttons are Observers and the document, view, window or application object is the Subject. MFC uses the polling technique to update these user-interface objects (Observers). Before a menu drops down or during the idle loop in the case of toolbar buttons, MFC routes an update command. The handler for the update command (defined using ON_UPDATE_COMMAND_UI) is called in the right object to enable/disable the menu item or the toolbar button. Using polling in this case is advantageous because of the following reasons:
- MFC can defer updating the user-interface objects till the occurrence of a specific event. Therefore, toolbar states can be updated when the application is idle and menu items can be updated when the menu drops down.
- Menu item or the toolbar button state purely depends on the current state of the Subject (state contained in document, view, window or application) and not its old state. Therefore, the user-interface objects need not update their states for every state change in the Subject.
MFC's Document/View Architecture
MFC's Document/View architecture uses the Observer Pattern. A document contains the data object and acts as a Subject. A view is a window object through which the user updates the document and it acts as an Observer. A document can have multiple views. Whenever the data in the document is changed by one of the views, it updates the document by calling UpdateAllViews method, with optional hint about the modification. To inform about the change to other views, the document object calls OnUpdate method for each view attached to it (except the view that called UpdateAllViews). Derived view classes can override the OnUpdate method and update themselves by querying the data from the document.
Model/View/Controller in Smalltalk
The first and perhaps the best-known example of the Observer Pattern appears in Smalltalk Model/View/Controller (MVC), the user interface framework in the Smalltalk environment. MVC's model class plays the role of Subject, while View is the base class for Observers.
Observer and Publish/Subscribe
Due to the object interaction, the Observer Pattern is also referred as Publish/Subscribe. Observers subscribe to the Subject for change notifications and in-turn the Subject publishes the state changes to the subscribed Observers. Publish/Subscribe (also called as Publisher/Subscriber) can be viewed as a variant of the Observer Pattern. Even though, the intent of both these Patterns is same, Publisher/Subscriber tries to address some of the implementation limitations of the Observer Pattern. In my next article, I will be talking about Publisher/Subscriber Pattern in detail.
Many objects work in unison behind a complex object oriented application. Handling the state dependencies between these objects is a major task. This article showed how the Observer Pattern could be used to maintain state consistencies. It started with a common programming problem, then it explained what, when and why the Observer Pattern is needed with a simple example. Benefits, liabilities, and known uses of the Pattern is also presented. The Observer Pattern can help to maintain the state consistencies between objects, and enhance the correctness and the quality of the system. However, it is not the silver bullet for solving all object interaction problems.
Special thanks to my friend Sree Meenakshi for her helpful suggestions in improving the clarity and presentation of this article.