This article, the second of the 5-part article on S.O.L.I.D design principles, is about the “O,” Open Closed principle made famous by Robert C. Martin (Uncle Bob)in his paper, https://web.archive.org/web/20150924054349/http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf.
S — Single-responsibility principle
O — Open-closed principle
L — Liskov substitution principle
Definition & References
In the original paper by Uncle Bob (linked above), OCP is described under the section Principles of Object-Oriented Class Design: The Open Closed Principle (OCP)
OCP deals with extending the capabilities of a module without changing it. Robert Martin in his blog https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html defines it as
You should be able to extend the behavior of a system without having to modify that system.
The most widely used and simplified definition of OCP is
A module should be open for extension but closed for modification.
Evolution of OCP
Initially, Bertrand Meyers suggested Inheritance as a way of implementing OCP. In his book “Object-Oriented Software Construction,” he writes, “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 a parent, adding new features. When a descendant class is defined, there is no need to change the original or to disturb its clients.”. Robert Martin defined a newer implementation of OCP, based on abstraction, which is today widely accepted as a better implementation. This article focusses on the Abstraction based OCP. Although for completion’s sake, we consider in this article how the Bertrand Mayer proposed OCP (Implementation Inheritence) stacks up against the Robert Martin version (Abstract interface).
Violation of OCP
As we can see above Car, pretty much does what is expected from Car. It can be started(),stopped() and driven(). However, Car, just like any object, model, or entity, is susceptible to change. Its behavior is destined to change. Suppose a client of Car decides to get an old, heritage car instead of the latest one. Or maybe he decides to splurge on a futuristic digital, electric hybrid car. Both the heritage car and the digital, electric car have different ways of start() & stop(). How do we support that?
With the help of a description like CarType, we can modify Car to support any type of Car
So here we see a clear violation of OCP. Car is being repeatedly modified. Every time a new kind of car is introduced(i.e., a change happens), Car is being altered. New test cases need to be written for this. Complex business logic in Car like calculation of distance and fuel consumed might get effected or might get broken. Even worse, If the Car module is part of a library or framework, the entire library or framework needs to be recompiled and released as a new version! Moreover, that’s not all. Every client of this framework or version now has to have this latest version. They might not have any need for a Car. Thus the penalty for violating OCP is huge. The impact of change is huge.
OCP via Implementation Inheritance
Bertrand Meyers’s solution was to simply not change the existing modules/class but to leverage Inheritance, one of the Object-Oriented Programmings key features. It meant don’t change a module, but inherit it and add whatever new feature needs to be added. Let’s see how our Car module would function with this.
Car now is untouched. So it was closed for Modification. Also, we have successfully extended it to create two different types of Car. HeritageCar & FutureCar. Each with its own unique way of start() and stop(). With this approach, we can support any new kind of Car in the future as well, without touching Car. Existing users of Car are blissfully unaware, and change is handled nicely. Thus we achieved OCP via Implementation Inheritance or the Bertrand Meyer way.
Problems with OCP via Implementation Inheritance
There is plenty of reference materials that document the problems with inheritance. One of the best articles to explain this can be found here. https://medium.com/hackernoon/inheritance-based-on-internal-structure-is-evil-7474cc8e64dc. So we won’t discuss that in this article. Since OCP via Inheritence uses Inheritence to achieve OCP, the problems with inheritance creep in. It is highly recommended that at this point the reader researches why Inheritance is susceptible to problems
OCP via Abstract Interfaces
The key aspect of implementing OCP via Abstract Interface is well, Abstraction! What Abstraction does is it hides the implementation. Implementations can be defined as concrete, different ways of implementing abstractions, so essentially abstractions are fixed while implementations are changing and evolving. For e.g., If we are to model the behavior of eating, we could define an abstraction called Eat(), which can be implemented in many different ways.
For e.g.
eatWithHand(),
eatWithSpoon(),
eatWithFork(),
eatWithChopSticks().
So if a class or module programs to abstraction as opposed to implementation we can safely say that it's insulated from change. Therefore the class or module is closed to change as it’s bound to a fixed Abstraction, not a defining behavior like a concrete implementation. E.g., A module/class known as Human can hide/evolve its way of eating behind an abstraction known as Eat. Depending on how he wants to eat, with a spoon, fork, etc. we can define a specific concrete implementation of eating. Look at it this way. A living being is always going to Eat. (Unless!) However, how he eats, what he eats, and when he eats are evolving and changing, and those things should be extended.
What we did to Car is we have defined it as an abstraction. Car types can change, its cost, its value, modes of operation all can change. So instead of programming to the ever-changing implementation/behavior, we now program to the Interface CarAbstraction which has abstractions like start(), stop(), drive().
So now our Car module is closed to modification and ever open to extension. Is there a new SpaceCar() or a new MoonCar()? Maybe one that runs on Water, a WaterCar(). No problem! Have it adhere to CarAbstraction and the cars keep rolling merrily. No change, no damage.
In https://www2.cs.duke.edu/courses/fall07/cps108/papers/ocp.pdf Robert Martin makes an important point about how it’s impossible to be prepared for all kinds of change. The very nature of the change which helps us prepare for it renders a designer helpless when it comes to Abstracting for all changes. Since change cannot be completely predicted, the next best thing is Relevance i.e which change is to be abstracted. Readers are encouraged to take their understanding to the next level for this by referring to the above work