DEV Community

Cover image for Open/Closed Principle
Maksim Ivanov
Maksim Ivanov

Posted on • Updated on

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 exactly does it mean…

That basically means that you should write your modules in a way that wouldn't require you to modify it's code in order to extend it's behavior.

open/closed principle

Let's Get To 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 recipy', 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 recipy', function(){
      const cookieMachine = new CookieMachine();

      expect(function(){ cookieMachine.makeCookie('unknown-cookie'); }).toThrow('Unknown cookie type.');
    })
  });
});
Enter fullscreen mode Exit fullscreen mode

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.';
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Let's imagine that it's Christmass season and we need to cook Pepper cookies. See, we violated OCP and now we have to change CookieMachine code and add new case block.

Let's Fix It

We'll introduce an abstraction, CookieRecipy:

class CookieRecipy{
  constructor(){
    // Sophisticated setup process
  }

  cook(){
    // Abstract cooking process  
  }
}

class ChocolateChipCookieRecipy extends CookieRecipy{
  constructor(){
    super();
    this.cookieType = 'chocolate-chip-cookie'
    // Sophisticated setup process
  }

  cook(){
    return 'Chocolate chip cookie';
  }
}

class FortuneCookieRecipy extends CookieRecipy{
  constructor(){
    super();
    this.cookieType = 'fortune-cookie'
    // Sophisticated setup process
  }

  cook(){
    return 'Fortune cookie';
  }
}

class PepperCookieRecipy extends CookieRecipy{
  constructor(){
    super();
    this.cookieType = 'pepper-cookie'
    // Sophisticated setup process
  }

  cook(){
    return 'Pepper cookie';
  }
}
Enter fullscreen mode Exit fullscreen mode

And also we'll 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.'
  }
}
Enter fullscreen mode Exit fullscreen mode

Great, now if we want to cook some new cookie – we just create new cookie recipy.

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 recipy', function(){
      const cookieMachine = new CookieMachine(new ChocolateChipCookieRecipy(), new FortuneCookieRecipy(), new PepperCookieRecipy());

      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 recipy', function(){
      const cookieMachine = new CookieMachine();

      expect(function(){ cookieMachine.makeCookie('unknown-cookie'); }).toThrow('Unknown cookie type.');
    })
  });
});
Enter fullscreen mode Exit fullscreen mode

Great, test pass now and we can cook ANY COOKIES WE WANT!

Discussion (3)

Collapse
enriquemorenotent profile image
Enrique Moreno Tent

While I understand the principle, I am unsure of what is the point of it. What are we trying to achieve following this principle? What is the benefit?

Collapse
satansdeer profile image
Maksim Ivanov Author

The point is to be able to extend system for cheap. So you won't have to make changes to your class/module every time you need to extend it. So this approach allows you to keep complexity low.

Collapse
enriquemorenotent profile image
Enrique Moreno Tent

Maybe i am missing something important here.

How does "not having to make changes to your classes" help to keep complexity low?

In the example you gave about the cookie machine, I would agree. It is a good way to extend the possibilities of the CookieMachine class, and it seems more flexible, but it actually seems to me like the complexity is bigger, than just adding a new "case".