Software engineering principles and patterns help us craft good clean software. One such pattern is an acronym we know as SOLID. “O” represents the open/closed principle simply defined: “open for extension, closed for modification”. Today I’m focusing on open/closed principle and how it pertains to modern application development.
“Classes, modules and functions should be open for extension, closed for modification”
— Bertrand Meyer
Design principles are sometimes difficult to interpret — not because they themselves are difficult, but more because the terminology and examples used are dated. Today’s post is second in a series that is my attempt to boil SOLID design principles down to modern layman’s terms with modern examples. Our first part of this five part series talked about “S” or single responsibility.
The five principles of SOLID are: Single Responsibility Principle (SRP), Open-closed principle, Liskov substitution principle, Interface segregation principle, and Dependency inversion principle.
Code for this post is found on my GitHub page. Modifications to show refactorings are in the add_employee_filter branch.
What is the Open/Closed Principle?
Before we start translating the open/closed principle, let’s look at what a few individuals in the industry have said about it. I previously quoted Bertrand Meyer who first conceptualized open/closed principle. Robert C. Martin had this to say about it:
Of all the principles of object oriented design, this is the most important. It originated from the work of Bertrand Meyer. It means simply this: We should write our modules so that they can be extended, without requiring them to be modified. In other words, we want to be able to change what the modules do, without changing the source code of the modules.
— Robert C. Martin (Design Principles and Design Patterns, 2000)
Uncle Bob (Robert Martin) later expanded on that definition in his blog post about the open/closed principle. Let me emphasize one statement made there: “You should be able to extend the behavior of a system without having to modify that system.” He previously points out that some of the original terminology seems outdated since many languages don’t necessarily require the modules be compiled.
Later on, another definition of the open/closed principle came about called The Polymorphic Open/Closed Principle. Wikipedia defines it: “Classes, modules and functions should have abstract interfaces, from whence all implementations are created”. See also Wikipedia (the source of all truth and knowledge).
I don’t want to spoil the beans here. That said, if you look at the open/closed as defined by the polymorphic version, some of our future parts in the series (L and I) will start to merge with this one.
An excerpt from OCP: The Open-Closed Principle [available here] by Robert C. Martin states: “When requirements change, you extend the behavior of such modules by adding new code, not by changing old code that already works.”
Ok, so what does it really mean?
In essence, the open/closed principle is saying that if you have to refactor your code to make changes, you’re violating it. That seems like a pretty harsh definition on it’s head but take some time to really think that through. You’ll start to realize that each SOLID principle is complementary one to another and adhering to them greatly reduces the code and maintenance complexity.
Ultimately what it really is trying to communicate is that your code should be built against abstractions. In this regard your code doesn’t care so much about *how* it gets or calls something as it is concerned about what it does with it. This does two things: 1) it pushes the implementation to something else (single responsibility) 2) allows you to swap implementations easily.
Ok, maybe that’s a bit absurd. Clearly applications do concern themselves with underlying technology. The takeaway here, however, is that they are only concerned with data within the implementations of that particular layer. All other layers are completely unaware and unconcerned with the stack.
Building your applications so they are technology agnostic is a perfect way of demonstrating open/closed. Changing database providers should not require you to modify higher level code in order to consume it.
Basic Example – Introduction
Let’s look at an example many of us come across in our career. Let’s say you have an interface IGetEmployee
. The first time around we only ever need to get an Employee record by ID so it only has IEmployee Get(int id)
. If I wanted this interface “open for extension” but “closed for modification”, what does that mean? Remember that this means we can extend the code but we don’t want changes to affect any other part of the system.
So now that we have IGetEmployee
, let’s also assume we have EmployeeController
implemented as a REST API.
Using abstractions
The example I’ve given highlights using interfaces to accomplish the open/closed principle. We can just as easily use base classes to build our code that follows the principle as well. That said, as I’ve previously mentioned, there are other principles that build on top of each other to make the whole enchilada. Given that, I’m ignoring abstractions for purposes of this post. I’m not saying don’t use them but I am saying there’s a better way.
Basic Example (overloads)
Expanding on our example, let’s say that a requirement later comes along that I need to be able to find employee records by some other criteria. If I were to change that original method then I would have breaking changes. Instead, I would add a new method or possibly an overload. In either case we’re extending the application but not modifying the existing signature. To wit, I might add an IEmployee[] Get(EmployeeFilter filter)
method to my interface.
By taking this approach we have extended our interface (and of course, our concrete implementation) but kept the existing signature intact so no other portion of the system has to change. Anything that was previously calling the original version is still completely valid.
One thing you might consider in this example as well In order to keep the code in sync I’d probably modify the implementation of IEmployee Get(ind id)
to call into the new overload. Purists would say that violates open/closed since we’re refactoring code on the original method. Personally I’m of the opinion that it actually reduces maintenance complexity. Unit tests ensure correctness of the application after such a refactor.
A cautionary tale here is that if you get too crazy with overloads you may eventually violate single responsibility principle (SRP). As a reminder, SRP has more to do with single business purpose than literally a single method.
Basic example (swapping implementations)
Perhaps one of the most prevalent examples in modern development is the concept of database repositories. To name a few options we could use MS SQL, MySql, Sqlite, Sybase, Oracle, Mongo, etc. SOLID applications do not concern themselves with underlying technology used to store their data. Their only concern are that they can save and retrieve data.
To demonstrate this I added a contrived InMemoryEmployeeRepository
based on the IEmployeeStore
from the WCF post. The code as committed to the branch has the Sqlite provider enabled. You can easily swap providers by commenting lines 36-37 in Startup.cs
and uncommenting line 40.
Conclusion
Design principles can sometimes be hard and scary to interpret. Frequently examples are dated and hard to understand with a modern twist.
Simply put the open/closed principle means that code should not be refactored. If we need to modify things to address new requirements we should extend functionality rather than refactor. This can be accomplished a variety of ways but I talked about method overloading and swapping implementations based on interfaces.
All code for this post can be found on my GitHub profile with modifications in the add_employee_filter
branch.
Credits
Photo by Nick Fewings on Unsplash