When trying to write clean JavaScript, there is a common problem that you can run into. You may have managed to separate your program's concerns into separate objects, yet still need object A to know if something happens to object B. Let's take a look at an example.
var jimGordon = {
arrest: function (criminal) {
console.log("Jim Gordon arrests " + criminal.name);
},
respondToCrimeBy: function (criminal) {
if (!criminal.superVillain) {
this.arrest(criminal);
} else {
console.log("Jim Gordon can't stop " + criminal.name);
}
}
};
(function () {
var batman = {
defeatSupervillain: function (villain) {
console.log("Batman handily defeats " + villain.name);
}
};
})();
var joeShmoe = {
name: "Joe Shmoe",
supervillain: false
};
jimGordon.respondToCrimeBy(joeShmoe);
// => "Jim Gordon arrests Joe Shmoe"
var mrFreeze = {
name: "Mr. Freeze",
superVillain: true
};
jimGordon.respondToCrimeBy(mrFreeze);
// => "Jim Gordon can't stop Mr. Freeze"
In this example, Jim Gordon is responsible for responding to crimes, and he can handle arresting your typical petty crooks, but he can't handle supervillains on his own. That responsibility belongs to Batman. But Batman doesn't respond to ordinary crimes directly. Gordon needs some way to notify Batman when he has a supervillain issue.
The problem is that while Batman knows how to get hold of Gordon, Gordon doesn't know how to get hold of Batman. That's where the observer pattern comes in.
var jimGordon = {
arrest: function (criminal) {
console.log("Jim Gordon arrests " + criminal.name);
},
respondToCrimeBy: function (criminal) {
if (!criminal.superVillain) {
this.arrest(criminal);
} else {
this.notifyObservers(criminal);
}
},
observers: [ ],
registerObserver: function (observerFn) {
this.observers.push(observerFn);
},
notifyObservers: function (data) {
this.observers.forEach(function(observerFn) {
observerFn(data);
});
}
};
(function () {
var batman = {
defeatSupervillain: function (villain) {
console.log("Batman handily defeats " + villain.name);
},
batSignal: function (villain) {
batman.defeatSupervillain(villain);
}
};
jimGordon.registerObserver(batman.batSignal);
})();
var mrFreeze = {
name: "Mr. Freeze",
superVillain: true
};
jimGordon.respondToCrimeBy(mrFreeze);
// => "Batman handily defeats Mr. Freeze"
Let's walk through how this works. By adding the observers
, registerObserver
, and notifyObserver
properties to Gordon, we've made him "observable," which means that if someone like Batman wants to know about what's happening in Gotham, they just need to give Gordon a way to let them know about it. That's what registerObserver
does. It takes a function that someone gives to jimGordon
and keeps it around in the observers
array. When something of interest happens, Gordon can notifyObservers
and pass along the information that he has.
That way, even though jimGordon
doesn't have direct access to batman
, Gordon can use the batSignal
method to let Batman know what is going on.
One of the important aspects of the observer pattern is that we can register multiple observers.
(function () {
var robin = {
defeatSupervillain: function (villain) {
console.log("Robin helps defeat " + villain.name);
},
radio: function (villain) {
robin.defeatSupervillain(villain);
}
};
jimGordon.registerObserver(robin.radio);
})();
var penguin = {
name: "Penguin",
superVillain: true
};
jimGordon.respondToCrimeBy(penguin);
// => "Batman handily defeats Penguin"
// => "Robin helps defeat Penguin"
Of course, there are other ways of implementing the observer pattern as well. In particular, the property names don't have to be the exact ones I chose here: observers
, registerObserver
and notifyObserver
. The important thing is the overall pattern allowing you to register and notify observers.
Now you have one more tool to keep your object design clean and well-factored.
Thanks for reading!
Josh Clanton