Open/closed principle in SOLID

By | December 24, 2024

It’s a popular question in interviews, and it seems that every interviewer has their own understanding of it. The most common answer is that you should write your class in a way that it is open for extension but closed for modification. Typically, this involves using polymorphism along with an abstract base class, allowing dynamic method invocation based on the passed concrete class. In other words, a class should be designed so that new functionality can be added without altering existing code. Thus, inheritance, polymorphism, and abstraction are often used to adapt to changing requirements without breaking existing functionality.

Bertrand Meyer originally described this principle in his book Object-Oriented Software Construction as follows:

“A software entity (class, module, function, etc.) should be open for extension but closed for modification.”

This principle applies not only to classes but to all software entities. Originally, it was written with respect to C++ and its RTTI mechanism, which enables polymorphism. Over time, I’ve seen various discussions advocating for replacing switch statements with inheritance and polymorphism. However, it’s important to remember that this is merely a principle, and the decision to use inheritance/polymorphism or if/else/switch constructs (I am joking) depends on the specific use case. For example, Go, a popular language, does not support object-oriented programming like Java or C# (it only has interfaces and structs). And switch is still used in projects like Kubernetes and Kafka 🙂 :

The simplicity of writing and reading code is critical. I’ve seen simple Java microservices with excessive use of classes and inheritance, making modifications unnecessarily complex. This principle, in my opinion, is particularly vital for frameworks like UI/MVVM frameworks or complex software expected to be actively extended in the future.

Continuing with the discussion of switch vs. inheritance/polymorphism, low-level logic such as the one found in this .NET WPF example demonstrates that switch statements may sometimes be more optimal. For instance, when writing an XML parser that distinguishes between element types, using switch often results in more efficient machine or bytecode, which is crucial for parser performance (I believe that this is clear for anyone).

Ultimately, it comes down to how this principle was perceived at the time it was introduced. Developers were dealing with a lot of code that needed to be rewritten simply because methods contained hardcoded logic in if statements, switch cases, and similar constructs. Additionally, in the 1980s and 1990s, build times and test execution times were significant, prompting developers to find design principles that minimized unnecessary work. This led to approaches like the Pimpl idiom in C++ and COM technology in Microsoft Windows, which were not typical for other languages and technology stacks.

Even during that era, it should be take into account the performance implications of using C++ RTTI in critical from performance point of view scenarios.

From the Robert Margin “Engineering Notebook columns for The C++ Report” key moments:

  1. Using abstract classes and polymorphism.
  2. Strong adherence to the principle: “The source code of such a module is inviolate. No one is allowed to make source code changes to it.”
  3. Experienced authors anticipating functionality expansions and designing abstract classes to accommodate future changes.

It’s worth noting that Robert Martin’s opinion on this principle has evolved over time, as he mentioned in his blog: An Open and Closed Case.

His books present the “closed for modification” statement in varying ways:

  • Agile Software Development: Principles, Patterns, and Practices:
    “Closed for modification. Extending the behavior of a module does not result in changes to the source or binary code of the module. The binary executable version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched.”
  • UML for Java Programmers:
    “You should be able to change the environment surrounding a module without changing the module itself.”

For me, the Open/Closed Principle is one of the practices that makes code more maintainable. To achieve this, we should:

  • Use abstractions like interfaces or abstract classes instead of concrete class types.
  • Leverage language features to dynamically determine, at runtime, which method should be invoked (polymorphism).
  • Write code in a way that allows behavior changes without altering inherited classes or library code.
  • Write methods so that when new requirements arise, there’s no need to repeatedly modify the same methods.

Code closed to modification offers several benefits:

  • Reduced likelihood of bugs: Changes to base classes already in use (e.g., in your code or by customers of your SDK) are less likely to introduce issues.
  • Minimized risk of breaking dependent code: Dependencies remain intact since unchanged code remains unaffected.

However, excessive use of abstractions can result in code that is difficult to read and maintain. It’s your responsibility to foresee how the code will be extended and to design abstractions accordingly. This process involves finding a balance between concreteness and abstraction.

If you’re unsure how to structure your code, start with concrete implementations without abstractions. Later, if the code undergoes repeated changes, you can introduce abstractions where needed.

There is also criticism of this principle that might be interesting to read:

Leave a Reply