We continue our journey through the SOLID principles by presenting the one that enhances the flexibility of applications: The Open/Closed Principle.
Many different approaches and principles can lead to long-term improvements in our code. Some of them are well-known
in the software development community, while others remain somewhat under the radar. In my opinion, this is the case
with The Open/Closed Principle, represented by the letter O in the word SOLID. In my experience, only those genuinely
interested in SOLID principles tend to understand what this principle means. We may have applied this principle without
even realizing it in some instances, such as when working with the Strategy pattern. However, the
Strategy pattern is just one application of the
Open/Closed Principle. In this article, we will delve into the full purpose of this principle, with all examples
provided in Go.
When we do not respect the Open/Closed Principle
You should be able to extend the behavior of a system without having to modify that system.
The requirement for the Open/Closed Principle, as seen above, was provided by
Uncle Bob in his
blog. I prefer this way of
defining The Open/Closed Principle because it highlights its full brilliance. At first glance, it may seem like an
absurd requirement. Seriously, how can we extend something without modifying it? I mean, is it possible to change
something without changing it? By examining the code example below, we can see what it means for certain structures
not to adhere to this principle and the potential consequences.
The example presents a single struct, AuthenticationService. Its purpose is to authenticate a User from the web
application’s Context, supported by the Gin package. Here, we have the main
method, Authenticate, which checks for specific authentication type associated with the data within the Context.
How User is retrieved from the Context may vary based on whether the User uses a bearer JWT token, basic authentication,
or an application key.
Inside the struct, we’ve included various methods for extracting permission slices in different ways. If we adhere to
The Single Responsibility Principle,
AuthenticationService should be responsible for determining if the authentication mean exists within the Context,
without being involved in the authorization process itself. The authorization process should be defined elsewhere,
possibly in another struct or module. So, if we intend to expand the authorization process elsewhere, we’d also need
to adjust the logic here.
This implementation leads to several issues:
AuthenticationService mixes logic initially handled in another location.
Any changes to the authentication logic, even if it’s in a different module, require modifications in AuthenticationService.
Adding a new method of extracting an User from Context always necessitates modifications to AuthenticationService.
The logic within AuthenticationService inevitably grows with each new authentication method.
Unit testing for AuthenticationService involves too many technical details related to different authentication methods.
So, once again, we have some code to refactor.
How we do respect The Open/Closed Principle
The Open/Closed Principle says that software structures should be open for extension but closed for modification.
The statement above suggests potential approaches for our new code, emphasizing the need to adhere to the Open/Closed
Principle (OCP). Our code should be designed in a way that enables extensions to be added from external sources. In
Object-Oriented Programming, we achieve such extensibility by employing various implementations for the same interface,
effectively utilizing polymorphism.
In the example above, we have a candidate that adheres to the Open/Closed Principle (OCP). The struct,
AuthenticationService, doesn’t conceal technical details about extracting a User from the Context. Instead,
we introduced a new interface, AuthenticationProvider, which serves as the designated place for implementing various
authentication logic. For instance, it can include TokenBearerProvider, ApiKeyProvider, or BasicAuthProvider.
This approach allows us to centralize the logic for authorized users within one module, rather than scattering it
throughout the codebase. Furthermore, we achieve our primary objective: extending AuthenticationService without
needing to modify it. We can initialize AuthenticationService with as many different AuthenticationProviders
Suppose we want to introduce the capability to obtain a User from a session key. In that case, we create a new
SessionProvider, responsible for extracting the cookie from the Context and using it to retrieve User from the
SessionStore. We’ve made it feasible to extend AuthenticationService whenever necessary, without altering its
internal logic. This illustrates the concept of being open to extension while closed for modification.
Some more examples
We can apply The Open/Closed Principle to methods, not just to structs. An example of this can be seen in the code below:
The function GetCities reads the list of cities from some source. That source may be a file or some resource on the
Internet. Still, we may want to read data from memory, from Redis, or any other source in the future. So somehow,
it would be better to make the process of reading raw data a little more abstract. With that said, we may provide a
reading strategy from the outside as a method argument.
As you can see in the solution above, in Go, we can define a new type that embeds a function. Here, we’ve created
a new type called DataReader, which represents a function type for reading raw data from some source. The
ReadFromFile and ReadFromLink methods are actual implementations of the DataReader type. The GetCities
method expects an actual implementation of DataReader as an argument, which is then executed inside the function
body to obtain raw data. As you can see, the primary purpose of OCP is to provide more flexibility in our code,
making it easier for users to extend our libraries without having to modify them directly. Our libraries become
more valuable when others can extend them without the need forking, pull requests, or modifications to the original code.
Thank you for the explanation! The Open/Closed Principle (OCP) is indeed a crucial SOLID principle, emphasizing the
importance of designing software in a way that allows for extension without modification of existing code structures.
It promotes the use of polymorphism and the creation of clear interfaces to enable this extensibility. OCP helps make
software more adaptable and maintainable as requirements change and new features are added.