First off, some official definitions for the Open-Closed Principle.
Bertrand Meyer originally defined the Open-closed Principle as
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification - Bertrand Meyer
Robert Martin later rephrased the principle when he coined the SOLID principles.
You should be able to extend the behavior of a system without having to modify that system. - Bob Martin
The Open-Closed Principle definitions are correct, but they can be difficult to picture if you don’t already know what the applications look like. We’ll get into plenty of examples soon, but I find it helpful to first think about motivations of the principle.
Motivating questions include
- How can we publish libraries that can adapt to unknown users without changing library code?
- In general, how do we accommodate unknown use cases?
- How do we prevent changes from cascading between services (or other kinds of components)?
- How does a service isolate it’s domain and support semantics needed by the caller?
- How can a service compose new dependencies without changing the service?
- Design stamina: How can code handle more use cases but remain simple?
I encourage you to pause for a few minutes and write down your thoughts about these questions. You don’t have to know the answers, guesses and questions are good too. Making predictions has been proven to improve learning.
Bertrand Meyer originally described this principle while considering how to make reusable software libraries. He wanted to publish some code that could be adapted to many systems without those systems modifying the library itself.
Think about all the packages you add to your software projects. Loggers, data access libraries, authentication, and much more. These libraries would not be meaningful if the source code had to be changed for every system.
Analogy: Function Parameters
Imagine if functions didn’t have parameters. They would be nearly useless. A new function would be needed for every combinations of inputs. There would be almost no potential for reuse or abstraction.
Parameters offer self-defined flexibility. The function owns the parameters, which work as a contract with the caller. The function implentation can consistently operate against this contract, yet many different callers can adapt a function to their own puposes by passing different data.
This same idea applies to components in our systems. We can design our components to accommodate diverse needs without anticipating the needs of each different caller. The component defines what flexibility it offers on its own terms (like with function parameters), but lets callers imprint their own meaning within that defined flexibility. This makes a system robust to change and powerful to reuse.
Components can offer this parameter-like open-closed flexibility many ways. First we’ll cover approaches with data, then with behaviors, with dependencies and with whole systems. We’ll also look at some examples that might look like OCP, but fail to deliver the expected value.