Sorting the OCP sheep from the OOP goats.
Stop being too quick on judging! OCP is not an OOP principle!
Today, let's talk about another piece of SOLID - OCP.
Again, like the other principles from SOLID, this one sounds quite understandable:
Software entities should be open for extension but closed for modification.
This principle was provided to us by the respected Bertrand Meyer, through his book “Object-Oriented Software Construction” back in 1988.
Initially, the idea was largely about starting to approach interfaces responsibly and stop breaking them, bringing suffering into the lives of clients.
Mr. Bertrand introduced us to the concepts of an Open Module and a Closed Module.
A module is considered open if it's available for extension - we can add fields to data structures, new methods/functions, and so on.
And a module should be considered closed when it already has users! For example, it's already being used by other modules. A closed module already has a clearly defined interface in terms of description.
When we start talking about this in the context of the OOP "world," the very essence is very similar to the idea of LSP - don't break the parent class, override methods -- inherit and add _new_ features.
Following OCP, we can extend or change the system without modifying already written code.
In "popular development," OCP is immediately associated with OOP and inheritance.
Look at two articles in Wikipedia about OCP - in English and Russian. If you don't know one of the languages, it's enough to simply translate with Google Translator, even such a translation will be enough to see the difference. This is either stupidity or cyber sabotage :)
Well, with OOP everything is clear. We have a parent class, or even an Abstract base class from which we inherit, and implement logic. Child classes reuse "basic" logic or extend it. All this is so that we can sleep peacefully and not touch the already working, original implementation.
Easy Peasy, everybody knows that.
What about the pure, functional world?
Programming functionally, we can follow OCP using higher-order functions and composition.
A higher-order function is a way of drawing a boundary between one behavior of the system and its remaining part. A way of abstraction, if you will.
This is a kind of alternative to inheritance because by passing other functions as parameters to this higher-order function of ours, we can change the highlighted behavior.
Of course, "internal" recursive calculations implementing some logic in terms of OCP are closed "modules," closed for extension. This means that we don't have any "inheritance of implementation" as in OOP.
This is not a problem because, thanks to function composition, we can model different behavior.
We do not change the implementation of our functions at all, but we can compose different functional models from them. For example:
In addition, algebraic data types are also very well suited to elegantly follow OCP.
In particular, sum types can be, well... summed, and this kind of naturally implies extensibility without changing what already exists.
An example of a sum type is a Rust enum:
In the future, we can add a new variant here and (most likely) not break the existing implementation. Of course, the Rust compiler will hit us on the head and force us to handle the new variant in all pattern matches.
And that's great!
For this reason, I don't give examples of Enums in Python - no matter how strong Python's type system is, and no matter how many cool, adult things we could emulate on it, there's more hope and faith in static typing.
Of course, all these examples are very close to code, but this doesn't diminish their value for understanding.
Although OCP can be expressed in applied pieces of code, first of all, I want to emphasize this:
OCP, like many other principles of software engineering, should first be considered and understood from a higher, design perspective.
In other words, OCP as a principle at its core has nothing to do with OOP at all. It's a principle of _designing_ software systems, in which we introduce the concept of a "black box" into our field of thinking, which we take as a software module that we can reuse, extend in one way or another in a functional sense, **but without changing its structure**.
In short, let's reinforce - adding a new feature by changing already written code is almost always BAD; Implementing a feature by writing new code (note, I'm not talking about the number of lines of this code) is almost always GOOD.
That's the whole essence of OCP ¯\_(ツ)_/¯
But! OCP is not a silver bullet, and it's impossible to follow it as a holy truth, well, you just can't. In software engineering, there are no such bullets at all, be it SOLID or something else.
In light of today's topic, trying to unreasonably blindly follow OCP, abstracting everything in a row, forgetting about the context of the project domain, and blurring it, we will only bury ourselves under a huge layer of crap code.
We need to remember OCP especially clearly when designing those parts of the system that are most likely to change.
And this, as it seems to me, in turn, means that "these very parts" should be somehow separated from the parts that most likely will not change at all.