Object-Oriented Huh?
The Open/Closed Principle is one of the least well-understood object-oriented software design principles in the industry. It deals primarily with one of the three fundamental pillars of object oriented design in an attempt to foster the creation of readily reusable code.
This post is part of a series on software design principles. As with all design principles, this is a general guideline, not a hard and fast rule. Following principles such as this will improve code quality, legibility, testability, and maintainability.
The Open/Closed Principle (OCP) is the O in the SOLID design principles acronym. It’s a simple premise whose terse and abstruse definition has led to some terrible misconceptions:
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Meyer, Bertrand
As with the 1st Amendment of the United States Constitution, there are many ways one might interpret these few short words. And, again as with the 1st Amendment, many of those interpretations are simply wrong.
More self-proclaimed software engineers than should ever be considered acceptable interpret the OCP in an incredible naïve way: once your code shipped, it shouldn’t be changed. For instance, you might have an extensible API that never changes. You commit your code, build an artifact, publish that artifact, and never come back. That certainly supports the words in the definition of the OCP, but not at all the meaning.
After all, unless one happens to be the best software engineer that ever lived, is still living, and will ever live – or they’ve just written the simplest and most straightforward algorithm ever known to humankind – then their code will have bugs in it. It’s a fact of life for software engineers. So if one were take this overly simplistic approach, then their code would suffer from design-time bugs for eternity. In order to address them, the developer would have to write a completely new module, and other developers would have to write completely new applications to consume it (because just refactoring to the new module would be a violation of this interpretation of the OCP).
The closest concept in computer science which this interpretation even approximates is version management. In particular, most version management schemes hold that published software should not be changed without an accompanying change in the module’s version. While this certainly makes for better software by protecting consumers from unexpected behavioral changes in third-party dependencies, it has nothing direct to do with the OCP.
Another overly simplistic interpretation deals with how to manage abstractions. In particular, if a module only takes input in the form of interfaces or abstract base classes, then that means that at runtime consumers of that module could pass in any number of radically different implementations of those inputs. This, in turn, results in wildly unexpected behaviors, which can fundamentally change – ie: modify – the module’s functionality. Simply put, this isn’t a valid interpretation simply because the logic in the module itself isn’t changing.
For example, consider a module that takes an instance of some interface and performs one action if the instance returns true and a different action if it returns false. Regardless of whatever business logic goes into any particular implementation of that interface – ie, when it returns true and when it return false – the main module doesn’t change. It will always perform one action when the interface instance returns true and the same different action when it returns false. The OCP isn’t really concerned with this interaction and how it changes software development: abstractions and their place in software design are covered by the Dependency Inversion Principle.
The OCP isn’t about how developers should manage source code file lifetimes. It’s not about software versioning schemes. It also has nothing to do with managing or avoiding abstractions.
So then, what is the Open/Closed Principle about?
First, it’s important to remember that the OCP is an object-oriented software design principle. That means it deals with one or more of the three pillars of OO: encapsulation, inheritance, and dynamic dispatch. In particular, the OCP deals with inheritance.
Bertrand Meyer coined the OCP in 1988. He wrote:
A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
A module will be said to be closed if [it] is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).
Meyer, Bertrand (1988)
Note that the end of the second premise specifically shies away from the meaning of “interface” in terms of an abstraction: that is, concrete types still have public APIs – which by definition is their interface – and everything else is hidden from external consumers.
The point of the OCP is simple: concrete types should be extensible. Meyer’s first premise above makes this clear by stating that, in general, it should be possible to inherit from any concrete type and add attributes (in the sense of OO fields and properties) and/or methods without having to change the original source code. This means that public concrete types should prefer not to be marked as final or sealed – or whatever the language’s immutability mechanism is – so that they can be inherited.
This is predominantly for concrete types since the availability and particulars of abstraction mechanisms differ from language to language. Generally speaking, languages that allow abstract types such as interfaces also permit extending them through the same inheritance mechanisms available for concrete types. However, the Interface Segregation Principle would prefer individual, isolated interfaces. This is because extending an interface requires implementations to implement considerably more attributes and methods than the implementor may desire, and can result in bloated and complicated APIs. Even sub-typing a new implementations from default/base concrete types can result in confusing inheritance hierarchies.
Meyer’s second premise simply means that at some point in its life-cycle, a software module should be published for consumption by third parties that may want to extend it. At this point, the module is closed simply because it can be consumed and extended without requiring access to the original source code. This is made easier by today’s plethora of package managers: npm, NuGet, Maven, RubyGems, etc. This is also where versioning comes into play, but the specifics of that are covered by the particular scheme chosen by the module’s developer.
The Open/Closed principle is easier than it seems: when writing public concrete types, prefer leaving them open to inheritance, and then compile and ship the code so it can be used and extended without the original source. That’s it. Nothing more complicated. No source code file management. No versioning. No mention of abstraction mechanisms. And certainly no magic incantation to be spoken over the code while the build process executes on a computer set inside a geometric shape made of burning candles.