Hi again,
If you don't know me, my name is Iaan Mesquita.
Today, we'll discuss about Observer Pattern - Behavioral Pattern.
Also known as: Event-Subscriber and Listener
I recommend you start from my last article (Factory Method: Simplifying Object Instantiation) because there are some concepts that are good to know before starting this.
Note: Like I said in the previous article, not always I'll use the best approaches to explain the pattern because I want to make the article simpler for those who don't know about this pattern
And also, it's good to have an understanding of Object-Oriented Programming (OOP) and SOLID principles.
Summary
As you know, before we start, let's take a look at a concept.
Concepts
Don't Repeat Yourself (DRY)
Don't Repeat Yourself (DRY) is a software development principle aimed at reducing the repetition of code patterns. The main idea behind DRY is to eliminate redundant code: if you have two or more identical code blocks in different parts of your software, you might be doing something wrong.
The main advantage of this principle is that it has better maintainability in the entire lifetime of your software. Imagine you have a lot of places inside your code that are the same, if you change the behavior of it you need to change it in a lot of places.
But, don't take this principle strictly true, you need to understand WHEN and HOW to apply it to work well. DRY shouldn't be a problem or concern for you.
We can discuss in the future a good example of "DRY(ing) in a bad way"
Problem
Taylor Alexis is a smart person, currently, she works in a big company that sells a lot of video games, but there are two that are the most sold and are very disputated: Playstation and Xbox.
And she knows these two brands have a lot of fanboys that can not hear the other brand's name because they get very mad.
But the business doesn't care and just wants to sell a lot of video games, whatever fanboys they are. These video games sell so fast but there is a waitlist that they subscribe to and get emailed when the video game has come into the store.
Despite the business needs to sell a lot, whatever kind of fanboys they were, it's not a good idea to send an email that they don't want to. Like: "THE BEST CONSOLE EVER HAS COME TO OUR STORE: PLAYSTATION / XBOX".
Resolving problem without Observer pattern
Taylor had been on vacation when everything happened, and the team decided to implement one solution for this:
import { Email, Whatsapp, SMS } from '../services'
class SendMessageToWaitlisteners {
constructor(
private readonly serviceName: string,
private readonly xboxSubscribersList,
private readonly playstationList
) {
if (serviceName == 'email') {
for (const contactInfo of xboxSubscribersList)
new Email(contactInfo.email).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: XBOX')
for (const contactInfo of playstationList)
new Email(contactInfo.email).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: PLAYSTATION')
}
}
}
But the customers started to complain because they don't use Email, they use WhatsApp and SMS and they wanted receive the message through these platforms. So the team implemented the other behaviors:
import { Email, Whatsapp, SMS } from '../services'
class SendMessageToWaitlisteners {
constructor(
private readonly serviceName: string,
private readonly xboxSubscribersList,
private readonly playstationList
) {
if (serviceName == 'email') {
for (const contactInfo of xboxSubscribersList)
new Email(contactInfo.email).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: XBOX')
for (const contactInfo of playstationList)
new Email(contactInfo.email).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: PLAYSTATION')
} else if (serviceName === 'whatsapp') {
for (const contactInfo of xboxSubscribersList)
new Whatsapp(contactInfo.numberPhone).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: XBOX')
for (const contactInfo of playstationList)
new Whatsapp(contactInfo.numberPhone).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: PLAYSTATION')
}
else if (serviceName === 'sms') {
for (const contactInfo of xboxSubscribersList)
new SMS(contactInfo.numberPhone).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: XBOX')
for (const contactInfo of playstationList)
new SMS(contactInfo.numberPhone).sendMessage('THE BEST CONSOLE EVER HAS COME TO OUR STORE: PLAYSTATION')
}
}
}
Even though everything was working fine, they realized that the code was so ugly, then they decided to isolate the part they think was repeating and isolate the message:
import { Email, Whatsapp, SMS } from '../services'
class SendMessageToWaitlisteners {
static createMessage = (consoleName) => `THE BEST CONSOLE EVER HAS COME TO OUR STORE: ${consoleName}`
constructor(
private readonly serviceName: string,
private readonly xboxSubscribersList,
private readonly playstationList
) {
const xboxMessage = SendMessageToWaitlisteners.createMessage('XBOX')
const playstatioMessage = SendMessageToWaitlisteners.createMessage('PLAYSTATION')
for (const contactInfo of xboxSubscribersList) {
if (serviceName === 'email')
new Email(contactInfo.email).sendMessage(xboxMessage)
else if (serviceName === 'whatsapp')
new Whatsapp(contactInfo.numberPhone).sendMessage(xboxMessage)
else if (serviceName === 'sms')
new SMS(contactInfo.numberPhone).sendMessage(xboxMessage)
}
for (const contactInfo of playstationList) {
if (serviceName === 'email')
new Email(contactInfo.email).sendMessage(playstatioMessage)
else if (serviceName === 'whatsapp')
new Whatsapp(contactInfo.numberPhone).sendMessage(playstatioMessage)
else if (serviceName === 'sms')
new SMS(contactInfo.numberPhone).sendMessage(playstatioMessage)
}
}
}
You may be thinking the code now is better (at least compared to before), but NO!
Of course, we have a problem with repetition but the most evident problem in this code is COUPLING.
What if the business needs to send different messages for each service? For instance, with WhatsApp, I would like to use some emojis, so would I create another variable and use it?
What if the business needs to implement another service, like messaging through Messenger? Should I create another conditional and implement the service?
What if the business needs to update another software that is interested in this behavior? Like, posting into a business's social networks? Should we create another conditional?
OMG, so chaotic. ๐คช
Observer Pattern
Taylor has come back from her vacation and went to her job. When she went there and started to work on code the team started being worried about her because they thought she would complain about code quality when would read. But she just says:
I know you guys tried your best with all constraints and context and I really appreciate it. Everything worked as expected, however, looking ahead, we need to improve this to help us in the future, especially because we probably will need to implement a lot of things in the future based on this behavior(Out-of-stock console arrived in-store). So, let's go guys, let's implement Observer Pattern.
The Observer Pattern is a Behavioral Design Pattern that allows an object (known as the "subject/observable/event/publisher") to publish changes to its state so that other objects (known as "observers/listeners/subscribers") can react accordingly.
Let's understand some concepts about the Observer pattern using our example below and cleaning up our code when it's possible:
Subject: This is an interface that declares a set of methods for managing subscribers.
In our example: Subject Interface
interface Subject {
addObserver(observer: Observer): void;
removeObserver(observer: Observer): void;
notifyAll(): void;
}
Observer: This is an interface or abstract class representing any object that needs to be notified of changes by the subject.
In our example: Observer Interface
interface Observer{
update(currentQuantityInStock:number):void;
}
ConcreteSubject: ConcreteSubject is a class that implements the Subject interface and contains the actual state or data that the observers are interested in. When this state changes, the observers need to be notified. In our case to simplify our example, we will just notifyAll when our state changes to positively.
In our example: PlaystationStockConcreteSubject and XboxStockConcreteSubject classes
class PlaystationStockConcreteSubject implements Subject {
private readonly observers: Observer[]
private _quantityInStock: number
constructor() { }
get quantityInStock(){
return this._quantityInStock
}
set quantityInStock(value){
this._quantityInStock = value
if(value > 0) this.notifyAll()
}
addObserver(observer: Observer): void {
this.observers.push(observer);
}
removeObserver(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notifyAll(): void {
for (let observer of this.observers) {
observer.update(this.quantityInStock)
}
}
}
class XboxStockConcreteSubject implements Subject {
private readonly observers: Observer[]
private _quantityInStock: number
constructor() { }
get quantityInStock(){
return this._quantityInStock
}
set quantityInStock(value){
this._quantityInStock = value
if(value > 0) this.notifyAll()
}
addObserver(observer: Observer): void {
this.observers.push(observer);
}
removeObserver(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notifyAll(): void {
for (let observer of this.observers) {
observer.update(this.quantityInStock)
}
}
}
ConcreteObserver: A class that implements the Observer. It will have a method to get updated data from the Subject.
To summarize things, let's assume that:
Xbox users only want to be notified by email and WhatsApp.
PlayStation users only by SMS.
The system wants to post on social networks if Xbox or PlayStation increases their stocks.
In our example: XboxEmailNotificator,XboxWhatsappNotificator,PlaystationSmsNotificator and SocialNetworkNotificator classes
class XboxEmailNotificator implements Observer{
constructor(
private readonly emailService,
private readonly emailList:string[]
){}
update(currentQuantityInStock: number): void {
this.emailService.send(`Hello, this is an email to let you know about our Xbox stock, we have ${currentQuantityInStock} available!!`,this.emailList)
}
}
class XboxWhatsappNotificator implements Observer{
constructor(
private readonly whatsappService,
private readonly numbersPhone:string[]
){}
update(currentQuantityInStock: number): void {
this.whatsappService.send(`Hey buddy ๐, come on to our store there is a lot of ${currentQuantityInStock} xbox available!! ๐๐`,this.numbersPhone)
}
}
class PlaystationSmsNotificator implements Observer{
constructor(
private readonly whatsappService,
private readonly numbersPhone
){}
update(currentQuantityInStock: number): void {
this.whatsappService.send(`Hey!!! There is ${currentQuantityInStock} PLAYSTATIONS available!! ๐๐`,this.numbersPhone)
}
}
class SocialNetworkNotificator implements Observer{
constructor(
private readonly facebookSDK
){}
update(currentQuantityInStock: number): void {
this.facebookSDK.send(`Stock Update! New consoles now available. Grab yours! ๐ #Restocked`)
}
}
And our client code may look like this:
//Fake data
const fakeEmails = ['iaan.123@gmail.com','another_guy@123xyz.com',"johndoe@johndoe.com"]
const fakeNumbers = ['+1999999999','+11231111111',"+167145349873648"]
//ConcreteSubjects
const xboxStockConcreteSubject = new XboxStockConcreteSubject()
const playstationStockConcreteSubject = new PlaystationStockConcreteSubject()
//ConcreteObservers
const xboxEmailNotificator = new XboxEmailNotificator(new EmailService(), fakeEmails)
const xboxWhatsappNotificator = new XboxWhatsappNotificator(new WhatsappService(), fakeNumbers)
const playstationSmsNotificator = new PlaystationSmsNotificator(new SmsService(), fakeNumbers)
const socialNetworkNotificator = new SocialNetworkNotificator(new FacebookSDK())
//Registering observers into subject
xboxStockConcreteSubject.addObserver(xboxEmailNotificator)
xboxStockConcreteSubject.addObserver(xboxWhatsappNotificator)
playstationStockConcreteSubject.addObserver(playstationSmsNotificator)
xboxStockConcreteSubject.addObserver(socialNetworkNotificator)
playstationStockConcreteSubject.addObserver(socialNetworkNotificator)
And when my state inside my concrete subjects is updated, so:
xboxStockConcreteSubject.quantityInStock = 9999
/**
* Email Sent: Hello, this is an email to let you know about our Xbox stock, we have 9999 available!!
* Whatsapp Sent: Hey buddy ๐, come on to our store there is a lot of 9999 xbox available!! ๐๐
* Facebook Post Sent: Stock Update! New consoles now available. Grab yours! ๐ #Restocked
*/
playstationStockConcreteSubject.quantityInStock = 25
/**
* Sms Sent: Hey!!! There is 25 PLAYSTATIONS available!! ๐๐
* Facebook Post Sent: Stock Update! New consoles now available. Grab yours! ๐ #Restocked
*/
Then, if just need to create a new behavior in a system that will execute if the stock is updated, I just need to create a new class that implements Observer and register it into a specific ConcretClass (Like PlaystationStockConcreteSubject)
I can also register/remove an observer at any time, even at runtime.
Pros & Cons
Pros:
Decoupling: The Observer pattern promotes a decoupled design where subjects and observers can interact without being tightly linked.
- Single Responsibility Principle:
- The Subject has the responsibility of managing its observers. It can attach, detach, and notify observers, but it doesn't concern itself with what these observers do with the information. This separation ensures that the Subject remains focused on its core task.
- Each Observer has its own responsibility for how it reacts to the notifications from the Subject. One observer might send notifications to customers, another might post on social network, and more. By keeping these responsibilities separate, the Observer Pattern adheres to the SRP.
- Open Closed Principle: You can introduce new types of Subjects/Observers into the program without breaking existing client code, just implementing the contracts.
Dynamic Relationships: Observers can be added or removed at runtime, offering dynamic relationships.
Broadcast Communication: A single subject can notify multiple observers without knowing who or what those observers are.
Cons
Overhead: The pattern can introduce overengineering, especially if not many components need to be observed.
Randomly Update: The subscribers will be updated but in a random order.
Unexpected Updates: Observers might receive updates they didn't expect, leading to potential issues.
Memory Leaks: If observers aren't properly removed, it can lead to memory leaks.
Conclusion
Thanks for reading.
We learned in this article that the Observer pattern can promote a more modular and scalable architecture, making systems easier to extend and maintain without coupling components.
However, remember this quote: "Not every problem is a nail, so use your hammer wisely."
I'm so excited to write these articles because I'm challenging myself to organize my thoughts, improve my writing (English is not my first language), and help people who are interested in these kinds of subjects.
As always, your feedback, suggestions, and critiques are welcome.
I hope you enjoyed it and I hope to see you in the next article.
Bye.
References
Refactoring.guru - Observer
Observer pattern - Wikipedia
Vocรช nunca mais vai conseguir ler um cรณdigo da mesma forma... - Filipe Deschamps
DRY - Wikipedia
Observer - Unity
Top comments (0)