The observer pattern is a design pattern that allows an object, called the subject, to maintain a list of its dependents, called observers, and notify them automatically when the subject’s state changes. This is useful when we have a situation where multiple objects are interested in the state of another object, but we don’t want to tightly couple them together. In this post we will lean it in deep using an example.
Introduction
Niku was building a smart home application called “Smarty” that would allow him to control devices such as lightbulbs, fans, and air conditioners using motion sensors and temperature sensors. The primary requirement was to turn on/off the devices in any room based on data received from the sensors. In other words, the physical sensors would send data to “Smarty”, and “Smarty” would control the physical devices based on the received data.
Nimi, Niku’s manager, had scheduled a design discussion meeting with Niku and asked him to prepare the design for “Smarty”. Niku was confident that he could impress Nimi with his object-oriented design.
Nimi asked, “Niku, are you ready with the design or do you need more time?“
Niku replied, “I am ready, Nimi. Shall I start explaining it?“
Nimi was impressed that Niku was ready in a short period of time and said excitedly, “Sure! I’m dying to hear it. Don’t make me wait any longer.”
Niku began to explain his design to Nimi.
“I had created a class called Device, which had a constructor to take an ID for the device, and two functions: turnOn() and turnOff().
I had also created three classes called Lightbulb, Fan, and AC, which inherited from the Device class.
Each of these classes also had a constructor to take an ID for the device, which was passed up to the Device constructor using a technique called inheritance. These three classes will have a public update method that can be called to inform the changes.
The lightbulb is dependent on occupancy only and so, its update method will need occupancy status. While the Fan & AC need occupancy as well as temperature value.“
Niku was very excited to explain to Nimi about the Controller class that he had designed.
He said- ” The Controller class will be the central control unit for all the devices in the Smarty. To make the code more modular and efficient, he had created instances of the Device class that we will have. e.g. Right now, as per requirement we have 4 devices i.e. two Lightbulbs, a Fan, and an AC. But if the requirement came to add more lightbulb or any other device, we will just add here. In this way the final code will be scalable.
This way, the Controller could easily access the devices and send commands to them. He further explained that this approach will make it easy to expand the system without having to change the core code.”
Overall, Niku has done a great job designing this system, and his attention to detail and focus on modularity will make it easier to maintain and expand in the future. He was thinking that he had made a very good impression, and Nimi was going to give him a big round of applause.
Nimi said, “Well done, Niku! But there are some issues with this approach.” Nimi then explained the issues in detail.
“This approach is not very modular and has some issues in its implementation. Adding device instances in the controller class is not a good idea, and we should try to decouple this part. Besides, the way we are updating the devices about the changes in the controller class is not up to the mark and goes against OOP’s concept of encapsulation. Currently, we are updating four devices here. What if someone has 50 lightbulbs in a big hall, 20 fans, and 10 ACs?
In that case, this approach is going to be a big problem, and we will need to add so many devices in the controller. So, I am not comfortable with this approach. To solve this problem, let me introduce you to our friend, the Observer Pattern.”
Technically, the observer pattern consists of three main components: the subject, the observer, and the concrete observer. The subject is the object that has some state that other objects may be interested in that state. The observer is an interface that defines the methods that the subject will use to notify the observers of changes. Finally, the concrete observer is the implementation of the observer interface that the subject will notify when its state changes.
Let us try to understand it using some example.
- Imagine that you are watching a trailer for a new web-series on Netflix. You’re really excited about it. But it is also not possible for you to check Netflix again and again every day. So, you want to be notified when it’s released so you can start watching it right away. Fortunately, Netflix offers an option for you to be notified when the series will be available.
Behind the scenes, Netflix is using the observer pattern to handle this notification system. When you click the “Notify Me” button, you are essentially registering yourself as an observer of the web-series release event. Netflix then adds you to a list of subscribers who are interested in this event.
When the web-series is released, Netflix triggers the event by notifying all subscribers on the list. This is where the observer pattern comes into play. Netflix acts as the subject, or the object that is being observed, and the list of subscribers are the observers. When the subject’s state changes, which in this case is the release of the web-series, it notifies all of its observers so they can take appropriate action.
In this scenario, the observer pattern is being used to allow interested users to be notified when a specific event occurs. It could also be used in other contexts where you want to be notified of changes in an object’s state, such as a stock price changing or a website going down. The observer pattern helps to decouple the subject from its observers, allowing for more flexible and scalable applications. - Imagine you have a weather app on your smartphone that shows the current temperature and weather conditions for your location. The app uses the observer pattern to notify you of any changes in the weather conditions.
The subject in this case is the weather data provider, which constantly updates the weather conditions for your location. The observers are the users who have the app installed on their phones and want to be notified of any changes in the weather conditions.
Whenever there is a change in the weather conditions, the weather data provider notifies all the registered observers (users) who have opted-in to receive notifications. The app then displays the updated weather information to the users.
The app also gives possibility to un-subscribe. So, if a user unsubscribe then he/she will not get the update. Here the weather app provide do not know each of its subscribers. IN fact he doesn’t need to know. He is just interested in a list, that need to be updated whenever some changes occur in the weather.
This way, the observer pattern helps to ensure that the app always displays the latest and most accurate weather information to the users, without the need for them to constantly check for updates themselves.
Niku stopped Nimi, and said – “So, you want to say that-
the Observer pattern is a design pattern that allows one object, called the “subject,” to notify other objects, called “observers,” automatically when a certain event occurs. In other words, the observers are “subscribed” to the subject and receive updates from it whenever there is a change.”
Nimi: “Yes, you understood it correctly. That is the reason why this pattern is also know as Pub-Sub Pattern i.e. Publisher-Subscriber Pattern“. She further explained – in this pattern the relationship between the subject and the observer is very interesting. It is one-to-many relationship. The publisher i.e. Subject is one but on the other hand there are many subscribers.
The Subject don’t want to know what the subscribers are going to do with his notification. And the subscribers don’t want to know how the Subject is getting the data. It is win-win for both parties.
In the mean time, Nimi got a notification on her laptop about the next meeting. She said – I had go to another meeting. Can we meet tomorrow with this new approach?
Niku: “So, in this case, the meeting event is the subject, and you are an observer. The subject (meeting event) maintains a list of its observers (people who have subscribed to the meeting event, just like you), and when the meeting time comes, it notifies all the observers on the list. You received the notification because you had subscribed to the meeting event and is thus an observer. Other people who have subscribed to the meeting event will also receive the same notification.?“
Both of them started laughing and scheduled a new meeting for the next day.
Niku was curious about his new friend “Observer Patterns”. He started reading about it in detail. You can also read it on Wikipedia using this link. He did not sleep that night and redesigned everything. Let us see what he has done.
Definition
As Observer pattern has mainly 3 elements i.e. the subject, the observer, and the concrete observer. So, the temperature and occupancy sensor will be the Subject class, which is responsible for maintaining a list of observers, registering new observers, removing observers, and notifying all observers when there is a change in the state of the subject.
The Observer class will have the notify method which will be called by the subject when there is a change in the state.
The lightbulb, fan, and AC will be concrete observer’s classes. These classes will implement the Observer interface and override the notify method to perform the necessary actions when they are notified by the subject. For example, when the temperature drops below a certain threshold, the lightbulb observer may turn on the lights, while the fan observer may increase the fan speed, and the AC observer may turn off the AC.
Observer Pattern Implementation
Let us try to implement it. First of all we need to implement the observer class. The observer class will have a virtual function called “update()”. After that we need to implement the Subject class:
Subject Class
We have defined a Subject class that represents the subject being observed by a set of Observer objects. The Subject class has three member functions:
- attach(Observer* observer): This function takes a pointer to an Observer object and adds it to the vector of observers that are interested in the subject. This function is used by observers to register themselves with the subject.
- detach(Observer* observer): This function takes a pointer to an Observer object and removes it from the vector of observers that are interested in the subject. This function is used by observers to unregister themselves from the subject.
- notify(): This function loops over all the registered observers and calls their update() function. This function is used by the subject to notify all its observers when there is a change in its state.
As a next step we will implement the Controller. This will be the base class and will be added to the devices like lightbulb, fan, AC etc.
Controller Class
The Controller inherits from the Subject class, meaning that it can attach, detach and notify observers. The Controller class has two private member variables: occupancy and temperature, which represent the occupancy status and temperature of a room or space.
The Controller class provides public getter and setter methods for these member variables. The getOccupancy() method returns the occupancy status, and the setOccupancy(bool occ) method sets the occupancy. After setting the occupancy it notify all. i.e. the notify function of the Subject call will be called where it will be calling the notify function of all the observers.
Similarly, the getTemp() method returns the temperature, and the setTemp(uint8_t temp) method set and notify all observers that the temperature has changed.
Observer Class
Before moving to the lightbulb, Fan and AC class, let us implement the Device class and the Observer class.
Observer is an abstract class with a pure virtual function update(), which means that any class that inherits from Observer must implement the update() function.
Device is a class that has a deviceId member variable of type uint32_t, and two member functions turnOn() and turnOff(). These functions will print a message indicating whether the device is being turned on or off, and update the state member variable accordingly.
Concrete Observer class
These are three subclasses that inherit from the Device class and implement the Observer interface: Lightbulb, Fan, and AC.
Each subclass takes in a Controller object as a parameter in the constructor, which is stored as a reference member variable called controller. This is so that the device objects can receive updates from the controller when the occupancy or temperature changes.
Each subclass also implements the update method from the Observer interface. This method is called by the Controller object when its state changes.
The Lightbulb subclass updates its state based on the occupancy in the room. If there is occupancy, the lightbulb turns on, otherwise it turns off.
The Fan and AC subclass updates its state based on the occupancy and temperature in the room. If there is occupancy and the temperature is greater than 20 or 25 degrees Celsius then the fan or AC turns on, otherwise it turns off.
Now we are done with the implementation of Observer pattern for the Smarty project. We hope Niku also has implemented it in similar fashion.
Next day, Nimi and Niku have started the discussion again.
Niku: “Thank you Nimi. Yesterday you had introduced very helpful and smart friend. I was not able to stop myself to design our product again.” Niku explained all the Design and implementation that we had done above.
Nimi: “Great! looks like you were burning the mid-night oil yesterday. I am quite impressed that you have done it so quickly. You have done a lot of work and I will not be able to say no if you ask for holidays :)”
Niku: “Yes, it was a lot of work, but it was worth it. I think the Observer pattern will make our product much more flexible and scalable. By separating the logic of the subject (Controller) and the observers (devices), we can add new devices easily without changing the subject code. Also, if we need to change the behavior of any device, we can do it without touching the Controller code.“
Nimi: “I agree that the Observer pattern provides flexibility, but it also has some drawbacks. For example, it can make the code more complex and harder to understand. Also, since the observers depend on the subject, they are tightly coupled. This can make testing and debugging more difficult. Another disadvantage is that it can increase the amount of memory used by the program, since each observer has to be stored in memory.”
Niku: “Those are valid points, Nimi. However, I think the benefits outweigh the drawbacks in our case. We need the flexibility and scalability provided by the Observer pattern. We can mitigate the complexity issue by properly designing and documenting our code. As for the memory usage, it should not be a problem since we are not dealing with a large number of observers. I will be sending you the minutes of this meeting. Thanks for your time.“
Niku has included the below table as a pros-cons of Observer Design Pattern
Pros & Cons
Pros | Cons |
---|---|
Promotes loose coupling between objects | Can result in excessive updates if not implemented carefully |
Supports a one-to-many relationship between objects | Can be more complex than other design patterns |
Allows for changes to be made to one object without affecting others | Can be difficult to debug if there are many observers |
Encourages modular and reusable code | Requires careful management of observers to prevent memory leaks |
Can simplify maintenance and testing | May require additional code to be written for each new observer |
Enables a flexible and extensible system | Can result in poor performance if too many observers are attached |
Scalability in Observer Pattern
So, we have learned the Observer pattern in detail. We can also add new code very efficiently. E.g. if we need to add new device type “Heater” then a small piece of code will be sufficient.
This class extends the Device class and implements the Observer interface. In the update() method, it retrieves the current temperature from the Controller and turns the heater on or off based on the temperature. If the temperature is less than 18 degrees Celsius, the heater is turned on, otherwise it is turned off.
Below is the complete code for your reference
Final Implementation
#include <iostream>
#include <cstdio>
#include <cstdint>
#include <vector>
#include <algorithm>
// Forward declaration of the Controller class
class Controller;
// Observer class
class Observer {
public:
virtual void update() = 0;
};
// Subject class
class Subject {
private:
std::vector<Observer*> observers;
public:
void attach(Observer* observer) {
observers.push_back(observer);
}
void detach(Observer* observer) {
auto it = std::find(observers.begin(), observers.end(), observer);
if (it != observers.end()) {
observers.erase(it);
}
}
void notify() {
for (Observer* observer : observers) {
observer->update();
}
}
};
// Subject class
class Controller : public Subject {
public:
bool getOccupancy() { return occupancy; }
void setOccupancy(bool occ) {
if (occ != occupancy) {
occupancy = occ;
notify();
}
}
uint8_t getTemp() { return temperature; }
void setTemp(uint8_t temp) {
if (temp != temperature) {
temperature = temp;
notify();
}
}
private:
bool occupancy = false;
uint8_t temperature = 0;
};
// Device class
class Device {
public:
Device(uint32_t id) : deviceId(id) {}
void turnOn() {
if(!state) {
printf("Device %X turned ON\n", deviceId);
state = true;
}
}
void turnOff() {
if(state) {
printf("Device %X turned OFF\n", deviceId);
state = false;
}
}
protected:
uint32_t deviceId;
bool state=false;
};
// Lightbulb class
class Lightbulb : public Device, public Observer {
public:
Lightbulb(uint32_t id, Controller& cont) : Device(id), controller(cont) {}
void update() {
bool occupancy = controller.getOccupancy();
(occupancy) ? (turnOn()) : (turnOff());
}
private:
Controller& controller;
};
// Fan class
class Fan : public Device, public Observer {
public:
Fan(uint32_t id, Controller& cont) : Device(id), controller(cont) {}
void update() {
bool occupancy = controller.getOccupancy();
uint8_t temp = controller.getTemp();
if(occupancy && temp > 20) {
turnOn();
} else {
turnOff();
}
}
private:
Controller& controller;
};
// AC class
class AC : public Device, public Observer {
public:
AC(uint32_t id, Controller& cont) : Device(id), controller(cont) {}
void update() {
bool occupancy = controller.getOccupancy();
uint8_t temp = controller.getTemp();
if(occupancy && temp > 25) {
turnOn();
} else {
turnOff();
}
}
private:
Controller& controller;
};
// Heater class
class Heater : public Device, public Observer {
public:
Heater(uint32_t id, Controller& cont) : Device(id), controller(cont) {}
void update() {
uint8_t temp = controller.getTemp();
if(temp < 18) {
turnOn();
} else {
turnOff();
}
}
};
int main() {
// Create the controller (temperature and occupancy sensor)
Controller controller;
// Create the observers (devices)
Lightbulb light1(1, controller), light2(2, controller);
Fan fan1(1, controller), fan2(2, controller);
AC ac1(1, controller), ac2(2, controller);
Heater heater1(1, controller);
// Subscribe the observers to the controller
controller.attach(&light1);
controller.attach(&light2);
controller.attach(&fan1);
controller.attach(&fan2);
controller.attach(&ac1);
controller.attach(&ac2);
controller.attach(&heater1);
// Simulate changes in the state of the controller
controller.setOccupancy(true);
controller.setTemp(30);
controller.setTemp(20);
controller.setOccupancy(false);
//To turn ON the heater. We had not added the check of occupancy so it will turn on
controller.setTemp(15);
controller.setTemp(19);
// Unsubscribe an observer (device)
controller.detach(&light2);
// Simulate more changes in the state of the controller
controller.setOccupancy(true); //Only Lightbulb 1 will be ON.
controller.setTemp(25);
return 0;
}
Related posts