In the previous article, we learned about the MVC architectural pattern using vanilla JS. In this article, we will take another step further to improve our data flow between model, view, and controller by implementing the Observer pattern.
In this article, we will:
- Explore ways to improve data flow for the existing MVC pattern.
- Learn about the problems that the Observer is designed to solve.
- Learn what the Observer pattern is all about.
- Explore how to implement the Observer pattern using example from the previous article.
In our MVC implementation, our controller mediates the interaction between the view and the model, i.e. the view triggers a change in the model through the controller, and then the model updates the view* through the controller. This setup is referred to as a two-way data flow.
With the Observer pattern, we can turn the model into an "observable" and the view into an "observer". When the observable is changed, it notifies the observer of its state, and the observer can react to that change. This makes our data flow unidirectional. The controller no longer updates the view, the view updates according to the model.
The Observer is a _ behavioral design pattern _ that lets you define a _ subscription mechanism _ to notify multiple objects about any events that happen to the object they're observing.
What problem does the Observer pattern solve?
It's sometimes helpful to use a real-world analogy in order to understand the kind of problem that this pattern is designed to solve. Let's just say that you really enjoy the content published on my blog.😉 Instead of having to visit my site every day to check for new articles (not that I'm against that idea 😜), you could choose to subscribe to my Facebook, Twitter, or Instagram pages so that you receive a notification every time I publish a new article. I'd suggest you subscribe to my blog, but it's not set up yet.😁 Anyway, you get the idea.
Use the Observer pattern when:
- Changes to the state of one object require changing other objects, and the actual set of objects that are to be updated based on the state change is unknown beforehand or changes dynamically. For example, you created a custom button class, and you want to let the clients hook some custom code to your button so that it fires whenever a user presses the button. You can add the subscription mechanism to your button class, letting the clients hook up their custom code via custom subscriber classes.
- Some objects in your app must observe others, but only for a limited time or in specific cases. The observer list is dynamic, so subscribers can join or leave the list whenever they need to.
What is the Observer pattern?
The Observer pattern consists of an observable/subject/publisher and an observer/subscriber.
-
Observable/Subject/Publisher is an object/class that maintains a list of observers/subscribers that it needs to notify when it is updated. It will also need to provide a subscription mechanism to allow the observers to subscribe or unsubscribe from its event streams. In reality, this mechanism consists of:
-
observers
: This class property holds an array of observers. -
addObserver()
: This method adds an observer to theobservers
array -
removeObserver()
: This method removes an observer from theobservers
array -
notify()
: This method notifies all observers that a change has happened.
-
-
Observer/Subscriber is any object/class that wants to subscribe/listen to changes in the _ publisher _ by implementing an
update()
method that will be called by the _ publisher's _notify()
method.
How to implement the Observer pattern?
In real-world applications, there might be several different subscriber classes/objects subscribing to the same publisher class. So, we wouldn't want to couple the publisher to all of those subscriber classes. That's why it's crucial that all subscribers implement the same interface and that the publisher communicates with them only through that interface. However, since we will be using Vanilla JS to keep things simple, we don't have access to interfaces. We can create parent classes from which we can extend. Specifically, the Observable
and Observer
parent classes will have those properties and methods required in order to implement the observer pattern.
Building up from our previous example of the MVC architectural pattern in the previous blog post, we will now implement the Observer pattern in order to achieve the unidirectional data flow.
- Create the
Observable
parent class with theaddObserver()
andremoveObserver()
, andnotify()
methods.
class Observable {
constructor() {
this.observers = [];
}
// Add an observer to this.observers.
addObserver(observer) {
this.observers.push(observer);
}
// Remove an observer from this.observers.
removeObserver(observer) {
const removeIndex = this.observers.findIndex((obs) => {
return observer === obs;
});
if (removeIndex !== -1) {
this.observers = this.observers.slice(removeIndex, 1);
}
}
// Loops over this.observers and calls the update method on each observer.
// The state object will call this method everytime it is updated.
notify(data) {
if (this.observers.length > 0) {
this.observers.forEach((observer) => observer.update(data));
}
}
}
export default Observable;
- Create the
Observer
parent class. At a bare minimum, it should include theupdate()
method.
class Observer {
// Gets called by the Subject::notify method.
update() {}
}
export default Observer;
- Update the
HeadingModel
class to have it extend theObservable
parent class.
import Observable from "./observable";
class HeadingModel extends Observable {
constructor() {
super();
this.heading = "Hello";
}
}
export { HeadingModel };
- Update the
HeadingView
class to have it extends theObserver
parent class. We will need to implement theupdate()
method that is required by the parent class and is called each time thenotify()
method in theObservable
class is called. We will also need to register theHeadingView
as an _ observer _ of theHeadingModel
_ observable _ via theaddObserver()
method.
import Observer from "./observer";
class HeadingView extends Observer {
constructor(controller) {
super();
this.controller = controller;
this.heading = document.getElementById("heading");
this.heading.innerText = controller.modelHeading;
this.heading.addEventListener("click", controller);
this.controller.model.addObserver(this);
}
update(model) {
this.heading.innerText = model.heading;
}
}
export { HeadingView };
- Finally, we need to update our controller's
clickHandler()
method to call theHeadingModel
'snotify()
method each time the event is emitted instead of changing theinnerText
through the controller.
class HeadingController {
constructor(model) {
this.model = model;
}
//EVENTLISTENER INTERFACE
handleEvent(e) {
e.stopPropagation();
switch (e.type) {
case "click":
this.clickHandler(e.target);
break;
default:
console.log(e.target);
}
}
get modelHeading() {
return this.model.heading;
}
//CHANGE THE MODEL
clickHandler(target) {
this.model.heading = "World";
this.model.notify(this.model);
}
}
export { HeadingController };
- Our
main()
function should still work as is.
Top comments (0)