Observer Design Pattern

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

If this definition doesn’t make complete sense right now😰, don’t worry.
By the time we reach the end of this blog, it will become clear and easy to understand 😄.

Now before we move ahead towards understanding the definition, let’s have an informal understanding of two terms that we are going to use throughout the discussion of this pattern:

  • Subject/Observable : A subject/observable is an object that is being observed for changes in it’s state.
  • Observer : An observer is an object that observes the subject/observable. So, whenever the state of subject changes, the observer has to be somehow notified about the change.

Now if we look back at the definition, what it informally says is,
One Subject will have many dependents(Observers) who would need to be notified if there is a change in the subject’s state.

Makes sense…but how🤔

Push v/s Pull

There are two ways in which an observer gets to know that there is a change in the subject:

  • Pull: The observers keeps asking(polling) the subject if there is a state change.
  • Push: The subject notifies all it’s observers if it has a state change.

While both the kinds of notifications have their own advantages and disadvantages, the observer design pattern generally revolves around the push notification.

Let’s understand this with the help of an example:

Let’s say you run a secret spy agency called the IMF (In case you have not watched Mission Impossible 🎬, it stands for the “Impossible Mission Force”). You have agents throughout the world, whom you want to inform about missions “Should they choose to accept”.

Let’s say you currently have the below agents awaiting their mission:

  • Ethan Hunt
  • Benji Dunn
  • Luther Stickell

We have the below classes for the given agents:

class EthanHunt {
    public void update(String mission) {
        System.out.println("Ethan Hunt received mission: " + mission);
    }
}

class BenjiDunn {
    public void update(String mission) {
        System.out.println("Benji Dunn received mission: " + mission);
    }
}

class LutherStickell {
    public void update(String mission) {
        System.out.println("Luther Stickell received mission: " + mission);
    }
}

Let’s say the IMF headquarters sends the mission information using the below class.

class IMFHeadquarters {

    private EthanHunt ethan = new EthanHunt();
    private BenjiDunn benji = new BenjiDunn();
    private LutherStickell luther = new LutherStickell();

    public void sendMission(String mission) {
        ethan.update(mission);
        benji.update(mission);
        luther.update(mission);
    }
}

So now, the client can send missions like:

public class Main {
    public static void main(String[] args) {

        IMFHeadquarters hq = new IMFHeadquarters();
        hq.sendMission("Retrieve the stolen AI chip.");
    }
}

Awesome😎. BUT…there are a few problems.

As you might have realized, if you want to add a new agent like Grace, you must modify the IMFHeadquarters class.

  • That means every new agent forces a change in the core mission distribution system, which quickly becomes unmanageable.
  • Also agents might leave the agency, which again forces a change in the core mission distribution system.

Loose Coupling

When two objects are loosely coupled, they can interact despite having very little idea about each other.
Observer design pattern acts as a good example of loose coupling due to the following reasons:

  • The only thing subject knows about the observer is that it implements a certain interface (the observer interface).
  • We can add or remove observers without making any changes to the subject.
  • Changes to either the subject or observer will not affect each other.

Alright then, let’s now look at the class diagram for Observer pattern:

Let’s now have a deeper understanding of the different situations that might occur in the IMF example context:

  • Let’s say Grace is now qualified to be an agent and needs to be added as an agent in IMF (in other words, an object comes and tells the subject that they want to become an observer).
  • Grace is now an official agent (observer) and now receives all missions (notifications) from IMF (subject).
  • Benji wants to retire (unsubscribe) and hence does not want to receive missions anymore.

In order to use the Observer design pattern, let’s understand who are the subject and observers in our example.
IMFHeadquarters notifies whenever there is a new mission, hence it is the subject.
Multiple agents (Ethan, Benji and Luther) are notified whenever there is a mission, hence they are observers.

Using the Observer pattern, we can decouple the IMFHeadquarters (subject) from the agents (observers).

Let’s start by setting up the observer interface (Agent) and it’s concrete implementations.

// Agent is the observer interface that every object has to implement to become a concrete observer
interface Agent {
    void update(String mission);
}
class EthanHunt implements Agent {
    public void update(String mission) {
        System.out.println("Ethan Hunt received mission: " + mission);
    }
}

class BenjiDunn implements Agent {
    public void update(String mission) {
        System.out.println("Benji Dunn received mission: " + mission);
    }
}

class LutherStickell implements Agent {
    public void update(String mission) {
        System.out.println("Luther Stickell received mission: " + mission);
    }
}

Now let’s setup the subject interface.

// IMF is the interface that the concrete subject has to implement
interface IMF {
    void addAgent(Agent agent);
    void removeAgent(Agent agent);
    void notifyAgents(String mission);
}

We can now have the concrete subject implement subject interface.

class IMFHeadquarters implements IMF {

    // Even though the concrete subject does not know specifics of concrete observers,
    // it knows that every observer will implement the Agent interface.
    private List<Agent> agents = new ArrayList<>();

    // The agents list is maintained by the addAgent/removeAgent methods.
    public void addAgent(Agent agent) {
        agents.add(agent);
    }

    public void removeAgent(Agent agent) {
        agents.remove(agent);
    }

    // All subscribed(not removed) agents are notified once a mission is created.
    public void notifyAgents(String mission) {
        for (Agent agent : agents) {
            agent.update(mission);
        }
    }

    public void createMission(String mission) {
        // Necessary code for creating the new mission
        notifyAgents(mission);
    }
}

Now clients can send missions like:

public class Main {

    public static void main(String[] args) {

        IMFHeadquarters hq = new IMFHeadquarters();

        Agent ethan = new EthanHunt();
        Agent benji = new BenjiDunn();
        Agent luther = new LutherStickell();

        hq.addAgent(ethan);
        hq.addAgent(benji);
        hq.addAgent(luther);

        hq.createMission("Retrieve the stolen AI chip.");
    }
}

And there you go🥁 …your first implementation of observer pattern, ready to help notify Ethan and team about all kinds of impossible missions 😅.

When not to use Observer Pattern ??

Now that we understand the Observer pattern, let us also understand if it is always the right choice ??
Well no… Suppose a famous celebrity makes a tweet or an Instagram post, in such a situation synchronously updating,

for each observer:
    observer.update()

could take huge amount of time and resources. This problem is often called the Celebrity Problem.

While the observer pattern works very well when dealing with a small number of observers, as the system grows and the number of observers increases, it becomes necessary to transition toward a more scalable event-driven architecture.

And with that, we’ve wrapped up the Observer Design Pattern – you’re now fully equipped with everything you need to know on Observers 🥳.