The Open-Closed Principle

Written by vadim-samokhin | Published 2017/12/01
Tech Story Tags: software-development | solid | open-closed-principle | requirements | oop

TLDRvia the TL;DR App

and what hides behind it

This is a second post (check the Single-responsibility principle here) on SOLID principles and what common foundation they all have.

Bertrand Meyer, 1988

Here is what Bertrand Meyers writes in his book “Object Oriented Software Construction” where this principle originates from:

A class is closed, since it may be compiled, stored in a library, baselined, and used by client classes. But it is also open, since any new class may use it as parent, adding new features. When a descendant class is defined, there is no need to change the original or to disturb its clients.

So he proposed using an implementation inheritance. Not good at all.

Robert Martin, 1996

Poor Robert Martin is often blamed for what actually Bertrand Meyer proposed. No wonder, look what he wrote in his paper “The Open-Closed Principle”:

1. They [Modules, as well as classes] are “Open For Extension”.This means that the behavior of the module can be extended. That we can make the module behave in new and different ways as the requirements of the application change, or to meet the needs of new applications.

2. They are “Closed for Modification”.The source code of such a module is inviolate. No one is allowed to make source code changes to it.

I realize that a good part of readers have read no further than that and stigmatized Robert Martin for using implementation inheritance. Well, it’s just an unfortunate naming. What follows is the elaboration of what he meant. And he’s quite clear about that:

Abstraction is the Key.

He demonstrates a simplistic example of a Client class depending directly on a concrete Server class:

class Client{public function doSomeWork(){return (new Server())->run();}}

Obviously, when Server class will have to be replaced, Client is affected. So the moral of the story: introduce abstractions and let your clients depend on them, instead of concrete implementations. And it’s all about the correct decomposing of the problem space, resulting in a bit of abstractions and quite a few of composable, loosely coupled and highly coherent implementations.

On a module level, this principle is applied best with an approach David Parnas described back in 1972. It enforces high cohesion, while the Open-closed principle suggests extracting abstractions, thus enabling loose coupling.

Robert Martin, 2003, 2004, 20132014

All the following years he has been striving to make clear his point of extending the behavior not via inheritance, but via swapping implementation of useful abstractions. Not very successfully: in 2017, some still don’t get it.

Class parameterization

I’ve listed two ways of introducing new classes without modifying existing code. The first one is using inheritance, which is discouraged, and the second one is extracting new interfaces so that they can be injected in a class. The third way is class parameterization, if I can call it so. It’s very close to introducing new abstractions, but it’s about injecting data instead of behavior. Consider the following example. I have a class that calculates employers’ salary. There could be different strategies, and this is reflected as the following (an example is simplified to stress the point):

class SalaryService{const TAX = 0.07;

**private $salaryStrategy**;  

**public function** \_\_construct(SalaryStrategy $salaryStrategy)  
{  
    $this->**salaryStrategy** \= $salaryStrategy;  
}  

**public function** calculate()  
{  
    **return** $this->**salaryStrategy**\->calculate(**self**::**_TAX_**);  
}  

}

So when a new strategy emerges, I won’t need to modify a SalaryService class. It means that OCP is not violated with respect to this requirement. But then you’re told that a tax changed — so you need to reflect it in a code. Would it be a violation of the Open-closed principle? Sure, since you need to modify existing code. What options do you have? Simple constant doesn’t represent any behavior, it represents data. Though you still can inject it via an argument, say, constructor parameter:

class SalaryService{private $salaryStrategy;private $tax;

**public function** \_\_construct(SalaryStrategy $salaryStrategy, $tax)  
{  
    $this->**salaryStrategy** \= $salaryStrategy;  
    $this->**tax** \= $tax;  
}  

**public function** calculate()  
{  
    **return** $this->**salaryStrategy**\->calculate($this->**tax**);  
}  

}

Violation of the Open-closed principle

The code above doesn’t violate OCP in the context of a newly emerged requirement. Basically, there is no such thing like just “violation of the Open-closed principle”. It can be violated only for some set of requirements. So the first version of SalaryService could have been aligned with the Open-closed principle for years, right until the moment when the tax changed.

Every time you need to implement new requirement and you modify existing code, it’s OCP violation. Of course, you can’t find out all your abstractions in advance — just because you can’t find out all the requirements in advance. So violating the Open-closed principle is OK — only if you’re extracting new abstractions, of course.

Summing it up

If any behavior, which is a part of some class, can change, then its implementation should hide behind an interface. This has a name already — it is loose coupling. But it can’t come up purely mechanically, by extracting a couple of interfaces. It all starts with domain decomposing.


Written by vadim-samokhin | Aspiring object-thinker
Published by HackerNoon on 2017/12/01