In this post we will discuss Open Closed Principle of Object Oriented Design in details, and review a real production code from Android Open Source Project in order to understand its importance.
This post is part of a series that describe SOLID principles of Object Oriented Design. If you have difficulties understanding the examples, please read the introduction article.
Definition of Open Closed Principle (OCP):
As formulated by Uncle Bob, Open Closed Principle states:
You should be able to extend a classes behavior, without modifying it.
Let’s try to understand what does this mean.
The first part of this definition talks about the behavior of a class. This behavior is defined by the source code of that class. Extending behavior probably means some kind of a change. Since the behavior is defined by the source code, then change of behavior requires change of code. Alright.
The second part of the above definition states that the class should not be modified. Modification of a class probably means changes to its source code. Therefore, source code should not be changed.
But this is a clear contradiction – we can’t both change the source code of a class in order to extend its behavior and not change it. Clearly, the above definition is either completely incorrect, or is just meaningless without additional contextual information.
However, this is not the only way Uncle Bob expressed OCP over the years. Maybe the other formulations are better?
In his paper from mid 90s, Uncle Bob adopted Bertrand Meyer’s definition:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
For me, this statement is even more puzzling than the previous one – it is broader (applies to all software entities), and uses a non-standard terminology (open for extension, closed for modification). For instance, I don’t know what does it mean for a function to be open for extension.
With all due respect to Uncle Bob (and I really mean it – half of the articles on this blog reference Uncle Bob’s ideas), I think that the above definitions of OCP are bad. These definitions are meaningless without additional context, and open a door for interpretations.
The irony is that after I read Uncle Bob’s books and articles, OCP turned out to be a perfectly sensible and useful concept. It is unfortunate choice of words in the above definitions that makes OCP hard to understand.
Open Closed Principle:
The main idea behind OCP is that it is possible to change the behavior of a system by replacing entities that constitute it with alternative implementations.
For example: imagine that the system uses classes that implement
TaxCalculator interface in order to calculate taxes. In such a system, replacing
UkTaxCalculator (assuming, of course, that both of them implement
TaxCalculator) will allow to easily adapt system’s tax calculation policy from US market to UK market. Note that this process does not require modification of the existing logic – all we need to do is provide a new implementation of an existing interface.
The above description should not come as a big surprise to anybody. It is a fundamental aspect of classes inheritance feature available in most object oriented languages.
However, the ability to change system’s behavior by adding new code and not modifying the existing one is not something you get out of the box. It is a result of careful design and implementation of so called “extension points” inside the application. Extension points are places where parts of the system can be easily removed or added. Introduction of these extension points into the code requires much work, increases code’s complexity and degrades readability. Therefore, there is a trade-off between simplicity and extensibility of the code.
In essence, OCP advocates for introduction of extension points. The ultimate outcome of following OCP is that application’s functionality can be extended by addition of new classes, without modification of any of the existing code.
Scope of Open Closed Principle:
In the previous section we understood the general idea behind OCP. Based on that description, you might be thinking now that OCP is a simple concept that is not that hard to implement in practice. Well, this is true and false at the same time.
From technical point of view, implementing OCP is really not that hard. There are numerous design patterns that have emerged over the years and can readily be used in order to implement extension points in the codebase (e.g. Strategy Pattern). The hard part of OCP is deciding where it should be applied.
But wait… Isn’t OCP a general principle that should always be applied? Well, OCP should always be applied, but this does not mean that it should be applied everywhere. We can think of OCP as having a scope which is smaller than the entire codebase. Application of OCP outside of its scope can negatively impact the quality of the code.
So, we need to understand what factors affect OCP’s scope.
First of all, there is no way to add functionality to the application without modifying any class at all. Therefore, even if the design follows OCP, when application’s behavior is being changed – some classes will be modified. In this context I would like to mention Dependency Injection architectural pattern. If the code follows Dependency Injection best practices, then swapping a class which is used at a pre-defined extension point requires change of just a single line of code in Construction Set.
Secondly, it is important to understand that OCP is only relevant in context of expected behavior extensions. It is simply impossible for a codebase to be protected from unexpected changes of requirements. Such changes can still lead to both small modifications, and big refactoring of the system. In extreme cases, unexpected changes of requirements can cause a complete rewrite of the entire application. Therefore, OCP depends on SRP – it can be applied only to “reasons to change” (responsibilities) that were envisioned at design time.
According to SRP, every class, ideally, will have one “reason to change”. Therefore, we should be able to add at least one extension point to every class.
But if every class will introduce extension point, then our code will turn into spaghetti of abstractions and design patterns, and addition of even the most trivial functionality will force us to write a bunch of classes, abstract classes and interfaces. Readability of such a codebase will be very poor, leading to major maintenance issues. In addition, if we add extension points everywhere, some of them will end up unused. Unused extension points are waste of time and effort. Therefore, adding extension points everywhere is clearly not the optimal approach.
The situation described above is not uncommon, though. Most of us had this experience of implementing extension points that ended up not being used. We invested time and effort in design, implementation and debug of these abstractions, and then had to maintain this entire set of rudimentary classes. Forever. Practice of addition of unused extension points falls into category of activities known as “gold platting”.
Let’s summarize our discussion so far. Until now we understood that OCP’s scope includes only behavior changes that can be expected ahead of time, and, even then, some code modifications will still be required. We also concluded that it is not a good idea to add extension points to every class. But, then, how do we decide which classes should follow OCP and introduce extension points?
This question brings us back to SRP and “reasons to change”. Since we can’t introduce extension points for all responsibilities, only those classes that implement the most unstable requirements should also implement extension points.
Open Closed Principle Rules of Thumb:
The notions of “requirements” in general and “most unstable requirements” specifically are very vague. I don’t think that it is really possible to know for sure which requirements will be the most unstable ahead of time. Therefore, in my opinion, preliminary introduction of extension points is counter-productive – there is very high chance that these extension points will end up being unused.
Does this means that I don’t follow OCP? No. It only means that I don’t try to guess where extension points will be needed. Instead, I wait until there is enough evidence that some part of a code can benefit from being refactored according to OCP. This evidence comes in a form of new features, change requests and bugs. If some classes need to be changed repeatedly in response to new requirements, then they should probably be refactored. Addition of extension points to classes that proved to be unstable usually saves a lot of effort in the long term and reduces the density of bugs in these areas.
Therefore, my personal “rules of thumb” when it comes to OCP are:
- Add extension points in classes that implement requirements which are intrinsically unstable due to instability in the business domain.
- Except for classes mentioned in #1, do not add any more preliminary extension points.
- Refactor parts of code and introduce additional extension points when these parts proved to be unstable.
Please note the first rule. As we said, guessing which requirements will be the most unstable is not a good practice. But rule #1 is not about guessing. It describes a case when we know with high degree of confidence that some requirements will be unstable because they reflect intrinsic instabilities in business domain.
For example: if we are designing banking application, we must introduce extension point for interest rate adjustment. Furthermore, this extension point should allow for changing of interest rate at runtime (e.g. by means of some external configuration file, GUI, API call, etc.). Just imagine bank’s reaction if they get application that doesn’t allow for interest rate adjustment, or needs to be recompiled and redeployed upon each change. This is clearly unacceptable.
The above example is very simple, though. Unfortunately, not all business domain instabilities are as evident as interest rate in banking. Therefore, it is very important for software engineers to learn about the business domain targeted by their application.
If we do learn about our business domain, and plant extension points accordingly, then we’ll get to experience this deep feeling of self satisfaction that arises when product manager submits huge change request which translates into just several hours of work on our end. The “huge” change request turns out to be simple to implement because there is an extension point for this specific case in the application that was planted ahead of time.
Open Closed Principle and Context:
Given that OCP is somewhat abstract principle that depends on SRP and subjective perception of unstable requirements, how can we know whether some design follows OCP? Honestly, I don’t think that there is an objective metric that can reflect adherence to OCP. What I can do, however, is use my personal “rules of thumb” introduced earlier as a metric for OCP. While I can’t argue that this metric is objective or otherwise universal, my own experience shows that it is not a bad metric.
According to my “rules of thumb”, developers violated OCP if they did one of the following:
- Failed to recognize an intrinsic instability in business domain and, consequently, failed to prepare the design for upcoming change that could be envisioned ahead of time.
- Introduced preliminary extension point that ended up being unused.
- Did not refactor the design according to OCP even after some requirements proved to be unstable.
Even given these specific types of OCP violation, it is still very difficult to impossible to say whether OCP was violated by
Context. For instance, only team members that worked on Android platform from the very beginning can say, in retrospective, something like: “we should have predicted that this or that would happen and plant an extension point for it”. I just don’t have the information required for this kind of analysis.
Furthermore, as we discussed in the article about SRP,
Context is a God Object. Since OCP applies to each reason to change (responsibility) separately,
Context would follow OCP only if each and any of its responsibilities would follow OCP. I’m definitely not going to review every one of
Due to above complications I decided to review just two distinct responsibilities of
Context – one that follows OCP and one that doesn’t.
Context implements so called Service Locator design pattern. Service Locator makes it possible to add new services without any modifications of the existing code. This responsibility clearly adheres to OCP. If this would be the only
Context‘s responsibility, it would be very much open for any future extension of system’s behavior that could be required.
But it is not the only
However, in practice, design of permissions management structure wasn’t changed at all. Additional methods for runtime permissions handling were simply added to
Context and its subclasses (and, for a totally mysterious reason, to
Fragment, but it is outside the scope of our examples). This is definitely one of the less sympathetic hacks I’ve ever seen. Instead of leveraging the opportunity to encapsulate permissions management into some
PermissionsManager, thus preparing the system for future changes of permission management policy, developers just hacked the new functionality into existing God Objects. There are no extension points in this scheme.
In my opinion, the aforementioned quick and dirty addition of runtime permissions management APIs to
Context is a clear violation of OCP: permission management scheme proved to be subject to major changes, but no design work or attempt to introduce extension points took place.
This post about Open Closed Principle got definitely longer than what I expected it to be. I guess it means that OCP is not a simple concept after all.
I hope that this writing helped you to understand what Open Closed Principle really is, and how to apply it in practice.