Open/Closed Principle
Originally posted on maksimivanov.com
OCP states that software entities (classes, modules, functions) should be open for extension, but closed for modification. Let’s figure out what that means exactly...
That basically means that you should write your modules in a way that wouldn’t require you to modify its code in order to extend its behavior.
Let’s Get To A Real World Example
I mean imaginary world example. Imagine you have a machine that can make chocolate-chip and fortune cookies.
describe('CookieMachine', function(){
describe('#makeCookie', function(){
it('returns requested cookie when requested cookie with known recipe',function(){
const cookieMachine = new CookieMachine();
expect(cookieMachine.makeCookie('chocolate-chip-cookie')).toEqual('Chocolate chip cookie');
expect(cookieMachine.makeCookie('fortune-cookie')).toEqual('Fortune cookie');
});
it('raises an error when requested cookie with unknown recipe', function(){ const cookieMachine = new CookieMachine();
expect(function(){ cookieMachine.makeCookie('unknown-cookie');}).toThrow('Unknown cookie type.');
});
});
});
Here is CookieMachine
itself:
class CookieMachine{
constructor(){
// Sophisticated setup process
}
makeCookie(cookieType){
switch(cookieType){
case 'chocolate-chip-cookie':
return 'Chocolate chip cookie';
case 'fortune-cookie':
return 'Fortune cookie';
default: throw 'Unknown cookie type.';
}
}
}
Let’s imagine that it’s Christmas season and we need to cook Pepper cookies. See, we violated OCP and now we have to change CookieMachine
code and add a new case
block.
Let’s Fix It
We’ll introduce an abstraction, CookieRecipe
:
class CookieRecipe{
constructor(){
// Sophisticated setup process
}
cook(){
// Abstract cooking process
}
}
class ChocolateChipCookieRecipe extends CookieRecipe{
constructor(){
super();
this.cookieType = 'chocolate-chip-cookie'
// Sophisticated setup process
}
cook(){
return 'Chocolate chip cookie';
}
}
class FortuneCookieRecipe extends CookieRecipe{
constructor(){
super();
this.cookieType = 'fortune-cookie'
// Sophisticated setup process
}
cook(){
return 'Fortune cookie';
}
}
class PepperCookieRecipe extends CookieRecipe{
constructor(){
super();
this.cookieType = 'pepper-cookie'
// Sophisticated setup process
}
cook(){
return 'Pepper cookie';
}
}
We'll also modify CookieMachine
to accept these recipes in constructor. We will use the reduce
method to reduce the recipes list to an object with cookie types for keys:
class CookieMachine{
constructor(...recipes){
this._recipes = recipes.reduce(function(accumulator, item){
accumulator[item.cookieType] = item;
return accumulator;
}, {});
}
makeCookie(cookieType){
if(this._recipes.hasOwnProperty(cookieType)){
return this._recipes[cookieType].cook();
}
throw 'Unknown cookie type.'
}
}
Great, now if we want to cook some new cookie — we'll just create a new cookie recipe.
Let's Update The Specs
Now, we have to pass cookie types upon CookieMachine
creation.
describe('CookieMachine', function(){
describe('#makeCookie', function(){
it('returns requested cookie when requested cookie with known recipe', function(){
const cookieMachine = new CookieMachine(new ChocolateChipCookieRecipe(), new FortuneCookieRecipe(), new PepperCookieRecipe());
expect(cookieMachine.makeCookie('chocolate-chip-cookie')).toEqual('Chocolate chip cookie');
expect(cookieMachine.makeCookie('fortune-cookie')).toEqual('Fortune cookie');
expect(cookieMachine.makeCookie('pepper-cookie')).toEqual('Pepper cookie');
});
it('raises an error when requested cookie with unknown recipe', function(){
const cookieMachine = new CookieMachine();
expect(function(){ cookieMachine.makeCookie('unknown-cookie'); }).toThrow('Unknown cookie type.');
})
});
});
Great, test pass now and we can cook ANY COOKIES WE WANT!