diskodev

DESIGN & DEVELOPMENT AGENCY

Observer Design Pattern

The Observer design pattern defines a one-to-many mapping between objects so that when one object changes state, all its dependents are notified and updated automatically.

This pattern is a behavioral pattern and makes it easier to 'observe' complex state of an object and be notified when it changes. The views and the model need not be tightly coupled with each other to observe the change in state. This pattern is commonly called Publish-Subscribe and is the cornerstone for Reactive Programming.

Examples

Some examples of this pattern are:

  • In a MVC application, the views observe the model (state) of the app and changes its representations accordingly
  • Stock ticker clients observe the price of a stock and gets notified when the price changes. The stock ticker can then change its views to reflect the latest price
  • A CI environment would observe a remote repository for any code changes and then run a build if new code was added to the repository

Why do we need this design pattern?

Consider a realtime / IOT kind of application. Here, the events are generated many times in a given period of time. You can design a system that frequently polls for the state - but this design brings with it a lot of difficulties. How frequently do you poll when you have no control of when the data changes? Poll very frequently and the model layer does little work apart from servicing such requests, poll less frequently and you might not work with the latest data. You also need to decide how the model and its various observers are to be coupled so that the observers are consistent with the model layer. These are some of the many design decisions that you need to take care of, that further increases the system's complexity.

In these type of applications, you can change your programming model from "pull" to "push". In the push model, you register your clients to the concerned model, and when the state changes, a notification is sent to the clients and the clients can update to the latest snapshot of the model and work with it.

When do you need to use this pattern?

  • When a change to one object requires others to be notified, so that they can use that data downstream to do other work
  • When you do not know how many or what objects need to be notified of the change in model state
  • When an object should be able to notify other objects without making assumptions about who or what these objects are. This makes the design loosely coupled and flexible to changes

Class structure and participants

The class structure and the participants of the Observer design pattern are as below,

Class Structure

Let us take a look at all the participants in this design pattern.

Subject: This class provides an interface for registering and deregistering observers. This has a list of observers that has registered with it.

Observer: This class defines an interface that gets called when an subject changes. Based on a subject's implementation, the new data gets pushed to the observer or it is the duty of the observer to get the refreshed state of the subject.

ConcreteSubject: This class is a subclass of Subject and contains the state which the observers are interested in. This sends a notification to its observers when its state changes.

ConcreteObserver: This class is a subclass of Observer and stores a copy of the subject's state that it is interested in. This has a reference to the subject so that, it can query the latest state of the subject. This object implements the interface that gets called when the subject state's changes.

How it works

  • The subject changes its state after some work/task
  • The subject notifies the observers, registered with it, of the need to update itself since the observer's state is now inconsistent with that of the subject. To do this, the subject's notify() method is called, which in turn calls the update() method of the observer
  • Depending upon the implementation, the new data is either pushed to the observer using the update() method or the observer queries for the new state of the subject. The information received from the subject is used to reconcile the observer's state

Example

Consider an environment, where, various clients need to monitor the price of a stock. The client has to display the correct price at all times and the price should be reflected in realtime. Now, consider an approach when the clients need to request the price from the server. This approach fails easily since we do not control the fluctuations in price and we would not know how frequently to poll for the price of a stock. This type of applications is made for the Observer pattern. We will use the stock model as the subject and have all the clients act as the observers. So, when the price of a stock changes, the observers get notified of the change in stock price and the observer can then get the new price from the model.

In code, the stock_model will be represented as:

class stock_observer;

class stock_model {
public:
    stock_model(float stockPrice);

    void register_observer(stock_observer *obs);
    void deregister_observer(stock_observer *obs);

    void set_price(float newValue);
    float get_price();

    void notify();

private:
    std::vector<stock_observer *> views;
    float price;
};

The model has methods that register, deregister observers and various getters / setters. The notify() is the method that gets called when the model's state (price) changes. 

As for the observer, the interface of stock_observer is as follows:

class stock_observer {
public:
    stock_observer(stock_model *);
    virtual void update() = 0;

    virtual ~stock_observer();

protected:
    stock_model *get_subject();

private:
    stock_model *model;
};

The observer has a reference to the model that it listens to. The model's notify() method is called when the price changes and this is the method that informs the clients that the price has changed. The clients then gets the new state from the model. The implementation of the notify() method of the model is as follows:

void stock_model::notify() {
    for (unsigned int i = 0; i < views.size(); ++i) {
       views[i]->update();
    }
}

The stock_view (client) will be represented as:

class stock_view: public stock_observer {
public:
    stock_view(stock_model *);
    ~stock_view();
    void update();

    float get_stock_price();

private:
    void update_price(float newPrice);

    float current_price;
};

The client's update() method is called from the model's notify() method and the update() method is implemented as:

void stock_view::update_price(float newPrice) {
    this->current_price = newPrice;
}

void stock_view::update() {
    update_price(get_subject()->get_price());
}

The various code manifests itself in the main() function as:

int main() {
    stock_model stock(17.43);

    stock_view desktop_client(&stock);
    stock_view mobile_client(&stock);

    std::cout << "Stock price (Desktop - moment 1) : " <<desktop_client.get_stock_price()<<std::endl;
    std::cout << "Stock price (Mobile - moment 1) : " <<mobile_client.get_stock_price()<<std::endl;

    stock.set_price(16.29);

    std::cout << "Stock price (Desktop - moment 2) : " <<desktop_client.get_stock_price()<<std::endl;
    std::cout << "Stock price (Mobile - moment 2) : " <<mobile_client.get_stock_price()<<std::endl;

    return 0;
}

In the main() function above, the model's initial value is 17.43 and the two clients observe the model. The price is printed onto the console. Then we make a change to the stock price. Once the model's price changes, the model's notify() method is called which in turn calls the observer's update() method, which syncs the new value and that new value is printed out to the console now. When you run the above program, the output you get is:

Stock price (Desktop - moment 1) : 17.43
Stock price (Mobile - moment 1) : 17.43
Stock price (Desktop - moment 2) : 16.29
Stock price (Mobile - moment 2) : 16.29
Deregistering observer...
Deregistering observer...

This is a simple overview as to how the Observer pattern works. The complete code can be viewed / downloaded from github.

Consequences of this pattern

  • The Observer pattern lets you decouple the subjects and observers. You can add as many observers without modifying the subjects. The subjects can be reused without reusing the observers and vice versa. All the subject knows, is that it has a list of observers, each conforming to a simple interface. The subject is not concerned with the concrete observer class
  • Any number of observers are supported by the subject. The subject notifies all observers under it of the change that has happened to its model
  • The model notifies of its changes which may cause a cascade of updates to observers and its dependents. The observers will be out of sync if the rules and criteria of the dependencies are not defined correctly

Implementation Notes

  • You need to decide on how you are going to store your observers. You can either optimize for speed or space. The application's high level requirements should drive this decision
  • When an observer registers with more than a single subject, the observer needs to know from which model the notify() method was called. This changes the definition of the observer's update() method. The new definition will be: virtual void update(subject *). The subject that caused the update is also passed to the client and hence it knows which model has changed
  • An important consideration is then - who triggers the call to a model's notify() method. An external operation that changes the model or is it the model itself? If it is the model, the advantage is that the clients do not have to remember to call the notify() method. The disadvantage is that consecutive operations will cause consecutive calls, which may be inefficient. If it is the external operation, the advantage is that the client can call the notify() method after it finishes a set of calls that updates the model. The disadvantage is that the external application needs to remember now to call the notify() method. If it forgets, then the new update is not pushed to the downstream clients
  • What happens when a subject is dereferenced? You need to provide a mechanism that allows the subject to notify the observers that the subject is being deleted and no further updates will be provided
  • The subject's state should be consistent before the call of the notify() method. If this is not the case, the state of the observers that query for the model's state will not be consistent with other observers
  • Should the changed payload be included in the update() method by the model? If so, the observer's update() method definition is as follows: virtual void update(subject *, payload *). The observer does not need to query for the state after the update() method. The new data is pushed to it. This also will help in get all the observers consistent with each other
  • If you are going with the above approach, it would also help if you include the type of update that occurs within the model. The type of update can be of any - insert, delete or edit
  • Sometimes in your application, you might need an object that can act as both a subject and as an observer. This is especially useful in Reactive Programming, where we need objects that can act as an observer and an observable

That's it.

You can view my projects on github.

You can follow me on twitter.