Understanding the Observer Design Patterns
There are 23 classic design patterns which are described in the original book Design Patterns: Elements of Reusable Object-Oriented Software. These patterns provide solutions to particular problems often repeated in software development.
In this article, I am going to describe how the Observer Pattern works and when it should be applied.
Observer Pattern: Basic Idea
Wikipedia provides us with the following definition:
The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. — Wikipedia
On the other hand, the definition provided by the original book is the following:
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. — Design Patterns: Elements of Reusable Object-Oriented Software
On many occasions we need to communicate system objects without coupling them either at code or at communication mechanism level. Should we have a group of objects (observers) that are required to be aware of the state of another object (observable), there are different techniques for carrying out the communication between them. The most popular techniques are:
-
Busy waiting. A process repeatedly verifies a condition. In our case, it would be an observer constantly checking whether or not the observable's condition has changed. This strategy could be a valid solution in certain cases, but it isn't an adequate solution for our scenario, since it would imply having several processes (observers) consuming resources without performing any operations, causing an exponential performance decrease in the number of existing observers.
-
Polling. In this case, the query operation is performed with a small window of time between operations. This is an attempt to implement synchronism between processes. However, we can once again appreciate degradation in the system's performance, furthermore, depending on the time set between each query, the information can be so delayed that it might be invalid causing a wastage of resources used by this technique.
The following codes show implementations of the previous techniques:
Busy-Waiting:
while(!condition){
// Query
if(isQueryValid) condition = true;
}
Polling:
function refresh() {
setTimeout(refresh, 5000);
// Query
}
// initial call, or just call refresh directly
setTimeout(refresh, 5000);
Although it isn't the goal of this post, it's a good idea to understand the two alternative techniques to this design pattern. Therefore, we can say that, in a nutshell, the difference between the active wait and polling techniques is that in the former the query operation is performed all the time, while in the latter there are intervals of time where the operation isn't executed.
Busy-Waiting:
while(resourceIsNotReady()){
//Do nothing
}
Polling:
while(resourceIsNotReady()){
Sleep(1000); // 1000 or anytime
}
The Observer pattern allows us to achieve a more efficient and less coupled code, since it avoids the previously mentioned issue, as well as having other advantages regarding code maintainibility. The UML pattern of this pattern is the following:
The classes that comprise this pattern are the following:
-
Subject is the interface that every observed class implements. This interface contains the
attach
anddetach
methods that allow us to add and remove observers from the class. It also contains anotify
method, which is responsible for notifying all of the observers that a change has occurred in the observed. Also, all of thesubject
s store references of the objects that observe them (observers
). -
Observer is the interface that all of the
ConcreteObserver
s implement. In this interface, theupdate
method is defined, which contains the business logic to be executed by each observer upon receiving the change notification from theSubject
. -
ConcreteSubject is the concrete implementation of the
Subject
class.
This class defines the state of theSubjectState
application, which must be notified when a change occurs. For this reason, the accessor methods (getState
andsetState
) are usually implemented, since they manipulate the state. This class is also responsible for sending the notification to all of its observers when the state changes. -
ConcreteObserver is the class that models each of the concrete observers. In this class the
update
method belonging to theObserver
interface is implemented, which is responsible for maintaining its state consistently which is responsible for keeping its state consistent with thesubject
objects it is observing.
Nowadays there's a family of libraries known as Reactive Extensions or ReactiveX which have made this design pattern popular. The Reactive Extensions make use of two design patterns: 1) Observer 2) Iterator. They also have a group of operators that use functional programming. These are some of the most popular Reactive Exntensions:
In these implementations, there are differences in the naming of classes and methods. The following names are the most extended:
-
Subscriber
corresponds with the classObserver
. -
ConcreteSubscribers
correspond with the classesConcreteObservers
. -
The
Subject
class is maintained. Theattach
anddetach
methods are renamed tosubscribe
andunsubscribe
. -
The
ConcreteSubjects
classes are concrete implementations, likeBehaviorSubject
,ReplaySubject
oAsyncSubject
.
Observer Pattern: Communication Strategies
There are two communication strategies between Subject
s (observables) and Observer
s (observadores) in the observer pattern:
-
Pull. In this model, the
subject
sends the minimum information to the observers and they are responsible for making inquiries to obtain more detail. This model focuses on the fact that theSubject
ignores theobservers
. -
Push. In this model, the
subject
sends the greatest amount of information to theobservers
the information of the change produced, regardless of whether they wanted it or not. In this model, theSubject
knows in depth the needs of each of itsobservers
.
Although a priori it may seem that the push communication technique is less reusable due to the fact that the Subject
must have knowledge about the observers
, this is not always the case. On the other hand, the pull based communication technique can be inefficient because the observers
have to figure out what changed without help from the Subject
.
Observer Pattern: When To Use
-
When there is a one-to-many dependency between system objects so that when the object changes state, all dependent objects need to be notified automatically.
-
You do not want to use [busy-waiting] (https://en.wikipedia.org/wiki/Busy_waiting) and [Polling] (https://en.wikipedia.org/wiki/Polling_(computer_science)) to update observers.
-
Decouple the dependencies between the
Subject
objects (Observables) and theObservers
(Observers) allowing to respect the Open-Closed Principle.
Observer Pattern: Advantages and Disadvantages
The Observer pattern has a number of advantages that can be summarized in the following points:
-
The code is more maintainable because it is less coupled between the observable classes and their dependencies (the observers).
-
Clean code since the Open-Closed Principle is guaranteed due to the new observers (subscribers) can be introduced without breaking the existing code in the observable (and vice versa).
-
Cleaner code because the Single Responsibility Principle (SRP) is respected since the responsibility of each observer is transferred to its
update
method instead of having that business logic in the Observable object. -
Relationships between objects can be established at runtime rather than at compile time.
However, the main drawback of the observer pattern, like most design patterns, is that there is an increase in complexity in the code, and an increase in the number of classes required for the code. Although, this disadvantage is well known when applying design patterns since the price to pay for gaining abstraction in the code.
Observer Pattern Examples
Next, we are going to illustrate two examples of application of the Observer pattern:
-
Basic structure of the Observer pattern. In this example we are going to translate the theoretical UML diagram into TypeScript code to identify each of the classes involved in the pattern.
-
An auction system in which there is an object (
subject
) that emits the change produced (push
technique) in theprice
of aproduct
that is being auctioned to all observers (observer
) interested in acquiring thatproduct
. Every time theprice
of theproduct
auction increases because some observer has increased the bid, it is notified to all observers.
The following examples will show the implementation of this pattern using TypeScript. We have chosen TypeScript to carry out this implementation rather than JavaScript — the latter lacks interfaces or abstract classes so the responsibility of implementing both the interface and the abstract class would fall on the developer.
Example 1: Basic structure of the observer pattern
In this first example, we're going to translate the theoretical UML diagram into TypeScript to test the potential of this pattern. This is the diagram to be implemented:
First, we are going to define the interface (Subject
) of our problem. Being an interface, all the methods that must be implemented in all the specific Subject
are defined, in our case there is only one ConcreteSubject
. The Subject
interface defines the three methods necessary to comply with this pattern: attach
, detach
and notify
. The attach
and detach
methods receive the observer
as a parameter that will be added or removed in the Subject
data structure.
import { Observer } from "./observer.interface";
export interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
There can be as many ConcreteSubject
as we need in our problem. As this problem is the basic scheme of the Observer pattern, we only need a single ConcreteSubject
. In this first problem, the state that is observed is the state attribute, which is of type number. On the other hand, all observers
are stored in an array called observers
. The attach
and detach
methods check whether or not the observer
is previously in the data structure to add or remove it from it. Finally, the notify
method is in charge of invoking the update
method of all the observers
that are observing the Subject
.
Objects of the ConcreteSubject
class perform some task related to the specific business logic of each problem. In this example, there is a method called operation
that is in charge of modifying the state
and invoking the notify
method.
import { Observer } from "./observer.interface";
import { Subject } from "./subject.interface";
export class ConcreteSubject implements Subject {
public state: number;
private observers: Observer[] = [];
public attach(observer: Observer): void {
const isAttached = this.observers.includes(observer);
if (isAttached) {
return console.log("Subject: Observer has been attached already");
}
console.log("Subject: Attached an observer.");
this.observers.push(observer);
}
public detach(observer: Observer): void {
const observerIndex = this.observers.indexOf(observer);
if (observerIndex === -1) {
return console.log("Subject: Nonexistent observer");
}
this.observers.splice(observerIndex, 1);
console.log("Subject: Detached an observer");
}
public notify(): void {
console.log("Subject: Notifying observers...");
for (const observer of this.observers) {
observer.update(this);
}
}
public operation(): void {
console.log("Subject: Business Logic.");
this.state = Math.floor(Math.random() * (10 + 1));
console.log(`Subject: The state has just changed to: ${this.state}`);
this.notify();
}
}
The other piece of this design pattern is the observer
. Therefore, let's start by defining the Observer
interface which only needs to define the update
method which is in charge of executing every time an observer
is notified that a change has occurred.
import { Subject } from "./subject.interface";
export interface Observer {
update(subject: Subject): void;
}
Each class that implements this interface must include its business logic in the update
method. In this example two ConcreteObserver
s have been defined, which will perform actions according to the Subject
s state. The following code shows two concrete implementations for two different types of observers: ConcreteObserverA
and ConcreteObserverB
.
import { ConcreteSubject } from "./concrete-subject";
import { Observer } from "./observer.interface";
import { Subject } from "./subject.interface";
export class ConcreteObserverA implements Observer {
public update(subject: Subject): void {
if (subject instanceof ConcreteSubject && subject.state < 3) {
console.log("ConcreteObserverA: Reacted to the event.");
}
}
}
import { ConcreteSubject } from "./concrete-subject";
import { Observer } from "./observer.interface";
import { Subject } from "./subject.interface";
export class ConcreteObserverB implements Observer {
public update(subject: Subject): void {
if (
subject instanceof ConcreteSubject &&
(subject.state === 0 || subject.state >= 2)
) {
console.log("ConcreteObserverB: Reacted to the event.");
}
}
}
Finally, we define our Client
or Context
class, which makes use of this pattern. In the following code the necessary classes to simulate the use of Subject
and Observer
are implemented:
import { ConcreteObserverA } from "./concrete-observerA";
import { ConcreteObserverB } from "./concrete-observerB";
import { ConcreteSubject } from "./concrete-subject";
const subject = new ConcreteSubject();
const observer1 = new ConcreteObserverA();
subject.attach(observer1);
const observer2 = new ConcreteObserverB();
subject.attach(observer2);
subject.operation();
subject.operation();
subject.detach(observer2);
subject.operation();
Example 2 — Auctions using Observer
In this example we're going to use the Observer pattern to simulate an action house in which a group of auctioneers (Auctioneer
) bid for different products (product
). The auction is directed by an agent (Agent
). All of our auctioneers need to be notified each time one of them increases their bid, so that they can decide whether to continue bidding or to retire.
Like we did in the previous example, let's begin by taking a look at the UML diagram that is going to help us identify each of the parts that this pattern is composed of.
The product
that is being auctioned is the Subject
's state, and all of the observer
s await notifications whenever it changes. Therefore, the product
class is comprised of three attributes: price
, name
and auctioneer
(the auctioneer that is assigned the product).
import { Auctioneer } from "./auctioneer.interface";
export class Product {
public price;
public name;
public auctionner: Auctioneer = null;
constructor(product) {
this.price = product.price || 10;
this.name = product.name || "Unknown";
}
}
The Agent
is the interface that defines the methods for managing the group of Auctioneer
s, and notifying them that the bid on the auctioned product has changed. In this case, the attach
and detach
methods have been renamed to subscribe
and unsubscribe
.
import { Auctioneer } from "./auctioneer.interface";
export interface Agent {
subscribe(auctioneer: Auctioneer): void;
unsubscribe(auctioneer: Auctioneer): void;
notify(): void;
}
The concrete implementation of the Agent
interface is performed by the ConcreteAgent
class. As well as the three methods previously described, which have a very similar behavior to the one presented in the previous example, the bidUp
method has been implemented, which, after making some checks on the auctioneer's bid, assigns it as valid and notifies all of the auctioneers of the change.
import { Agent } from "./agent.interface";
import { Auctioneer } from "./auctioneer.interface";
import { Product } from "./product.model";
export class ConcreteAgent implements Agent {
public product: Product;
private auctioneers: Auctioneer[] = [];
public subscribe(auctioneer: Auctioneer): void {
const isExist = this.auctioneers.includes(auctioneer);
if (isExist) {
return console.log("Agent: Auctioneer has been attached already.");
}
console.log("Agent: Attached an auctioneer.");
this.auctioneers.push(auctioneer);
}
public unsubscribe(auctioneer: Auctioneer): void {
const auctioneerIndex = this.auctioneers.indexOf(auctioneer);
if (auctioneerIndex === -1) {
return console.log("Agent: Nonexistent auctioneer.");
}
this.auctioneers.splice(auctioneerIndex, 1);
console.log("Agent: Detached an auctioneer.");
}
public notify(): void {
console.log("Agent: Notifying auctioneer...");
for (const auctioneer of this.auctioneers) {
auctioneer.update(this);
}
}
public bidUp(auctioneer: Auctioneer, bid: number): void {
console.log("Agent: I'm doing something important.");
const isExist = this.auctioneers.includes(auctioneer);
if (!isExist) {
return console.log("Agent: Auctioneer there is not in the system.");
}
if (this.product.price >= bid) {
console.log("bid", bid);
console.log("price", this.product.price);
return console.log(`Agent: ${auctioneer.name}, your bid is not valid`);
}
this.product.price = bid;
this.product.auctionner = auctioneer;
console.log(
`Agent: The new price is ${bid} and the new owner is ${auctioneer.name}`
);
this.notify();
}
}
In this problem there are four different types of Auctioneer
defined in the AuctioneerA
, AuctioneerB
, AuctioneerC
and AuctioneerD
classes. All of these auctioneers implement the Auctioneer
interface, which defines the name
, MAX_LIMIT
and the update
method. The MAX_LIMIT
attribute defines the maximum amount that can be bid by each type of Auctioneer
.
import { Agent } from "./agent.interface";
export interface Auctioneer {
name: string;
MAX_LIMIT: number;
update(agent: Agent): void;
}
The different types of Auctioneer
have been defined, to illustrate that each one will have a different behavior upon receiving the Agent
s notification in the update
method. Nevertheless, all that has been modified in this example is the probability of continuing to bid and the amount they increase their bids by.
import { Agent } from "./agent.interface";
import { Auctioneer } from "./auctioneer.interface";
import { ConcreteAgent } from "./concrete-agent";
export class ConcreteAuctioneerA implements Auctioneer {
name = "ConcreteAuctioneerA";
MAX_LIMIT = 100;
public update(agent: Agent): void {
if (!(agent instanceof ConcreteAgent)) {
throw new Error("ERROR: Agent is not a ConcreteAgent");
}
if (agent.product.auctionner === this) {
return console.log(`${this.name}: I'm the owner... I'm waiting`);
}
console.log(`${this.name}: I am not the owner... I'm thinking`);
const bid = Math.round(agent.product.price * 1.1);
if (bid > this.MAX_LIMIT) {
return console.log(`${this.name}: The bid is higher than my limit.`);
}
agent.bidUp(this, bid);
}
}
import { Agent } from "./agent.interface";
import { Auctioneer } from "./auctioneer.interface";
import { ConcreteAgent } from "./concrete-agent";
export class ConcreteAuctioneerB implements Auctioneer {
name = "ConcreteAuctioneerB";
MAX_LIMIT = 200;
public update(agent: Agent): void {
if (!(agent instanceof ConcreteAgent)) {
throw new Error("ERROR: Agent is not a ConcreteAgent");
}
if (agent.product.auctionner === this) {
return console.log(`${this.name}: I'm the owner... I'm waiting`);
}
console.log(`${this.name}: I am not the owner... I'm thinking`);
const isBid = Math.random() < 0.5;
if (!isBid) {
return console.log(`${this.name}: I give up!`);
}
const bid = Math.round(agent.product.price * 1.05);
if (bid > this.MAX_LIMIT) {
return console.log(`${this.name}: The bid is higher than my limit.`);
}
agent.bidUp(this, bid);
}
}
import { Agent } from "./agent.interface";
import { Auctioneer } from "./auctioneer.interface";
import { ConcreteAgent } from "./concrete-agent";
export class ConcreteAuctioneerC implements Auctioneer {
name = "ConcreteAuctioneerC";
MAX_LIMIT = 500;
public update(agent: Agent): void {
if (!(agent instanceof ConcreteAgent)) {
throw new Error("ERROR: Agent is not a ConcreteAgent");
}
if (agent.product.auctionner === this) {
return console.log(`${this.name}: I'm the owner... I'm waiting`);
}
console.log(`${this.name}: I am not the owner... I'm thinking`);
const isBid = Math.random() < 0.2;
if (!isBid) {
return console.log(`${this.name}: I give up!`);
}
const bid = Math.round(agent.product.price * 1.3);
if (bid > this.MAX_LIMIT) {
return console.log(`${this.name}: The bid is higher than my limit.`);
}
agent.bidUp(this, bid);
}
}
import { Agent } from "./agent.interface";
import { Auctioneer } from "./auctioneer.interface";
import { ConcreteAgent } from "./concrete-agent";
export class ConcreteAuctioneerD implements Auctioneer {
name = "ConcreteAuctioneerD";
MAX_LIMIT = 1000;
public update(agent: Agent): void {
if (!(agent instanceof ConcreteAgent)) {
throw new Error("ERROR: Agent is not a ConcreteAgent");
}
if (agent.product.auctionner === this) {
return console.log(`${this.name}: I'm the owner... I'm waiting`);
}
console.log(`${this.name}: I am not the owner... I'm thinking`);
const isBid = Math.random() < 0.8;
if (!isBid) {
return console.log(`${this.name}: I give up!`);
}
const bid = Math.round(agent.product.price * 1.2);
if (bid > this.MAX_LIMIT) {
return console.log(`${this.name}: The bid is higher than my limit.`);
}
agent.bidUp(this, bid);
}
}
Finally, let's show the Client
class, which makes use of the observer pattern. In this example, an auction house is declared, with an Agent
and four Auctioneer
s, where two different products (diamond
and gem
) are being auctioned. In the first auction, all four auctioneers participate. In the second auction, the D
class auctioneer retires leaving the three remaining to participate.
import { ConcreteAgent } from "./concrete-agent";
import { ConcreteAuctioneerA } from "./concrete-auctioneerA";
import { ConcreteAuctioneerB } from "./concrete-auctioneerB";
import { ConcreteAuctioneerC } from "./concrete-auctioneerC";
import { ConcreteAuctioneerD } from "./concrete-auctioneerD";
import { Product } from "./product.model";
const concreteAgent = new ConcreteAgent();
const auctioneerA = new ConcreteAuctioneerA();
const auctioneerB = new ConcreteAuctioneerB();
const auctioneerC = new ConcreteAuctioneerC();
const auctioneerD = new ConcreteAuctioneerD();
concreteAgent.subscribe(auctioneerA);
concreteAgent.subscribe(auctioneerB);
concreteAgent.subscribe(auctioneerC);
concreteAgent.subscribe(auctioneerD);
const diamond = new Product({ name: "Diamond", price: 5 });
concreteAgent.product = diamond;
concreteAgent.bidUp(auctioneerA, 10);
console.log("--------- new Bid-----------");
concreteAgent.unsubscribe(auctioneerD);
const gem = new Product({ name: "Gem", price: 3 });
concreteAgent.product = gem;
concreteAgent.bidUp(auctioneerB, 5);
console.log(`The winner of the bid is
Product: ${diamond.name}
Name: ${diamond.auctionner.name}
Price: ${diamond.price}`);
console.log(`The winner of the bid is
Product: ${gem.name}
Name: ${gem.auctionner.name}
Price: ${gem.price}`);
Finally, I've created two npm scripts
, through which the code presented in this article can be executed:
npm run example1
npm run example2
GitHub Repo available here.
Conclusion
Observer is a design pattern that allows respecting the Open-Closed Principle since new Subject
and Observer
can be created without breaking the existing code. In addition, it allows communication between two actors of the system without the need for them to be linked in the knowledge of each other. Finally, the performance degradation that occurs in more elementary techniques such as busy-waiting and polling is overcome.
Finally, the most important thing about this pattern is not the concrete implementation of it, but being able to recognize the problem that this pattern can solve, and when it can be applied. The specific implementation is the least of it, since it will vary depending on the programming language used.