Monster Open-Close Principle
ref – https://medium.com/@severinperez/maintainable-code-and-the-open-closed-principle-b088c737262
The chief benefit of the OCP is maintainability. If you adhere to the OCP you can greatly decrease future maintenance costs. The opposite applies as well — when you don’t adhere to the OCP, future maintenance costs will be greater. Consider how the coupling of two entities affects their respective maintainability. The more a given entity knows about how another one is implemented, the more we can say that they are coupled. Therefore, if one of the two entities is changed, then the other must be changed too. Here is a simple example:
We create a MonsterManager that takes in an array of monsters and locations. It then calls rampageAll on it and has all the monsters rampaging through all the locations.
|
// Monster Types and Manager var MonsterManager = { init: function(monsters, locations) { this.monsters = monsters; this.locations = locations; }, getRandomLocation: function() { function getRandomInt(max) { return Math.floor(Math.random() * Math.floor(max)); } return this.locations[getRandomInt(this.locations.length)]; }, |
But in order execute the Monster’s abilities, we must first check the monster. Then execute whatever special functionality it has. In order to do this, we check for the prototype of the Monster.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
rampageAll: function() { this.monsters.forEach(function(monster) { var location = this.getRandomLocation(); if (Object.getPrototypeOf(monster) == Kaiju) { console.log( "The " + monster.type + " " + monster.name + " is rampaging through " + location + "!" ); } else if (Object.getPrototypeOf(monster) == GreatOldOne) { console.log( "The " + monster.type + " " + monster.name + " has awaken from its slumber in " + location + "!" ); } }, this); } }; |
Now we create literal objects in order for instances of the monsters to be created
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
var Kaiju = { init: function(name) { this.name = name; this.type = "Kaiju"; return this; } }; var GreatOldOne = { init: function(name) { this.name = name; this.type = "Great Old One"; return this; } }; |
Create the monsters and locations
|
// Rampage! var monsters = []; var locations = ["Athens", "Budapest", "New York", "Santiago", "Tokyo"]; var rodan = Object.create(Kaiju).init("Rodan"); monsters.push(rodan); var gzxtyos = Object.create(GreatOldOne).init("Gzxtyos"); monsters.push(gzxtyos); var myMonsterManager = Object.create(MonsterManager); myMonsterManager.init(monsters, locations); |
Then start rampaging!
|
myMonsterManager.rampageAll(); // Logs: (with variable city names) // The Kaiju Rodan is rampaging through Santiago! // The Great Old One Gzxtyos has awaken from its slumber in Athens! |
In this snippet we use the OLOO pattern to define a MonsterManager prototype object and two types of monster prototypes, Kaiju and GreatOldOne. After initializing some monsters and an array of locations, we then initialize a new MonsterManager called myMonsterManager and call its rampageAll method, unleashing our monsters on those unlucky cities the randomLocation method happens to choose (sorry!) Can you spot any problems in this code related to OCP adherence?
Take a look at the rampageAll method — right now it iterates over each monster and checks whether they are of type Kaiju or GreatOldOne and then logs an appropriate message. What happens when this monster-filled world surfaces some new and terrible type of monster? In order for the program to work we would have to add another branch of conditional logic to the rampageAll method. In other words, we would have to modify the source code and therefore break the OCP. Doing so would not be a big deal with just one more monster type, but what about 10 new types? Or 20? Or 1,000? (Apparently this poor world is filled with monsters!) In order to extend the behavior of our MonsterManager (that is, let it deal with more types of monsters) we are going to have to think about how we deal with individual monster types.
Ultimately, the MonsterManager probably shouldn’t care about how each different monster rampages, so long as it has the ability to rampage in some fashion. Implementing our program this way would allow us to abstract away the rampage functionality to each individual monster. In other words, we can extend the functionality of the rampageAll method without changing the source code of MonsterManager. This use of abstraction is often described as a sort of contract — the objects being used promise to implement some piece of functionality and the object using them promises not to care how they do it. In this case, each monster promises to have a rampage function and MonsterManager promises to let them handle the details.
The correct approach
|
function ImplementationError(message) { this.name="Implementation Error"; this.message = message; } ImplementationError.prototype = new Error(); |
We connect its prototype to a new Error object
In order to inherit from JS’s Error object
Let’s first create a literal object
it sets the interface for all components to follow
we declare that we have an init property and rampage init
This means that all components who conform to this will need to implement these two functions
|
var MonsterInterface = { init: null, rampage: null, } |
Bladmaster is an object that has __proto__ pointing to MonsterInterface
We want this to act as a prototype object
|
var Bladmaster = Object.create(MonsterInterface); |
we then conform to our Monster Interface and implement:
init
rampage
|
Bladmaster.init = function(name) { this.name = name || "This Bladmaster"; this.type = "Bladmaster"; this.ability = "Cyclone Spinning Blade"; return this; } Bladmaster.rampage = function(location) { console.log(this.name + ' executed ' + this.ability + ' at ' + location); } |
Now we have Bladmaster prototype ready. We can create many instances of the Kaiju Monster
Let’s create a small group
we create an instance where the instance’s __proto__ point to to Bladmaster.
this enables us to inherit Bladmaster propeties
|
const katara = Object.create(Bladmaster); const meerak = Object.create(Bladmaster); const samuro = Object.create(Bladmaster); |
But wait a minute, we need to make sure Bladmaster REALLY conforms to MonterInterface. Not just take their word for it.
prototypeObject is our Bladmaster
interfaceObject is MonsterInterface
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
function createWithInterfaceValidation(prototypeObject, interfaceObject) { // so for every key in our MonterInterface: init, rampage // index 0: key is init // index 1: key is rampage Object.keys(interfaceObject).forEach(function(key) { // if init DOES NOT exist as a property for Kaiju // OR // if init IS NOT A function console.log('key is: ', key); console.log('function is: ', prototypeObject[key]); if (prototypeObject[key] === null || typeof prototypeObject[key] !== "function") { // then we throw an error throw new ImplementationError( "Required method " + key + " has not been implemented." ); } }); // init and function MUST BE functions // IFF then we will use it as a prototype to create an object return Object.create(prototypeObject); } |
So we validate our Bladmaster according to MonsterInterface
|
const monsters = []; let Gorn = createWithInterfaceValidation(Bladmaster, MonsterInterface); monsters.push(Gorn.init("Gorn")); let Samuro = createWithInterfaceValidation(Bladmaster, MonsterInterface); monsters.push(Samuro.init("Samuro")); let Tonak = createWithInterfaceValidation(Bladmaster, MonsterInterface); monsters.push(Tonak.init("Tonak")); |
Now that we have our monsters, we need to create a Monster Manager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
var MonsterManager = { init: function(monsters, locations) { this.monsters = monsters; this.locations = locations; }, getRandomLocation: function() { function getRandomInt(max) { return Math.floor(Math.random() * Math.floor(max)); } return this.locations[getRandomInt(this.locations.length)]; }, rampageAll: function() { this.monsters.forEach(function(monster) { var location = this.getRandomLocation(); monster.rampage(location); }, this); } }; |
We then create our locations and then add in both monsters and locations.
MonsterManager then proceeds to call rampageAll.
|
var locations = ["Athens", "Budapest", "New York", "Santiago", "Tokyo"]; MonsterManager.init(monsters, locations); MonsterManager.rampageAll(); |
MonsterManager’s rampageAll’s implementation will proceed to have each monster execute their abilities upon the city:
|
rampageAll: function() { this.monsters.forEach(function(monster) { var location = this.getRandomLocation(); monster.rampage(location); }, this); } |