Maintainable Code and the Open-Closed Principle
In part 1 of the SOLID series, we learned about how to write more flexible code with the Single Responsibility Principle (SRP). By isolating pieces of functionality in individual classes/modules the SRP helps us guard against unnecessarily coupling responsibilities. If the implementation of one responsibility changes, SRP-adherent design prevents the change from affecting other responsibilities. However, decoupling responsibilities does not necessarily mean a complete decoupling of classes/modules, functions, objects, etc. In most object-oriented code, different objects must deal with one another in some fashion. What then happens when a particular object needs to be changed? As with responsibility changes, this poses a challenge for the maintenance of downstream objects that could inadvertently be affected by the change. One way to reduce the impact of this challenge is to adhere to the second of the SOLID principles: the Open-Closed Principle (OCP).
A Quick Refresher on SOLID
SOLID is an acronym for a set of five software development principles, which if followed, are intended to help developers create flexible and clean code. The five principles are:
- The Single Responsibility Principle — Classes should have a single responsibility and thus only a single reason to change.
- The Open/Closed Principle — Classes and other entities should be open for extension but closed for modification.
- The Liskov Substitution Principle — Objects should be replaceable by their subtypes.
- The Interface Segregation Principle — Interfaces should be client specific rather than general.
- The Dependency Inversion Principle — Depend on abstractions rather than concretions.
The Open Closed Principle
Robert C. Martin, creator and chief evangelist of SOLID, credits Bertrand Meyer as the originator of the OCP. In his 1988 book Object Oriented Software Construction, Meyer describes the need to develop flexible systems that can adapt to change without breaking. To do this, Meyer advocates the design of systems where entities (classes, modules, functions, etc) are “open for extension, but closed for modification”. In his development of the SOLID principles, Martin runs with this idea, describing it as a “straightforward” attack against the threat of “fragile, rigid, unpredictable and un-reusable” code . For his part, Martin breaks down the OCP into its two constituent parts, defining code that is “open for extension” as code to which you can add new behavior, and code that is “closed for modification” as code that is “inviolate” in that it’s design should never be changed once implemented. In other words, the OCP says that you can always add new code to an object, but should never change the design of old code.
The chief benefit of the OCP is maintainability. If you adhere to the OCP you can greatly decrease future maintenance costs. The opposite applies as well — when you don’t adhere to the OCP, future maintenance costs will be greater. Consider how the coupling of two entities affects their respective maintainability. The more a given entity knows about how another one is implemented, the more we can say that they are coupled. Therefore, if one of the two entities is changed, then the other must be changed too. Here is a simple example:
In this snippet we have a simple function called
announce that takes an object as an argument and uses that object’s
description properties to log a message to the console. When we call this function and pass it the
favoriteCities object we get the expected output. But what if we decide that we don’t want the
favoriteCities object to store its
items in an array and decide it’s better to store them in an object?
By changing our
favoriteCities.items implementation from an array to an object we effectively broke our
announce function. The reason is that the
announce function knows too much about how
favoriteCities was implemented and expects it to have an
items property that is an array. Fixing this would be relatively trivial (perhaps we could add a conditional to the
announce function to check first whether the
collection.items property is an array or an object), but at what long-term cost? What if we didn’t make this change until much later in development and we had lots of functions that used
collection.items? We would then have to add conditionals to every place that referenced
A better solution is to use polymorphism and to let each
collection object decide for itself how its
items should be iterated over and logged. In this pattern, the
announce function doesn’t care whether the collections it works with use arrays, objects, or some other data structure to hold their
items. Here is one approach:
In this final snippet, we provide
favoriteCities with a
logItems method that implements how to log its items. As far as
announce is concerned, it can deal with any collection object so long as it has a
description property and a
logItems method. This is the OCP in action — the
announce function is extensible because it can handle any
collection that guarantees these two properties but it is also closed to modification because we don’t have to change the source code in
announce to change its available behaviors.
Abstractions as Extensions
In a 2014 blog article, Martin discusses the apparent paradox in writing entities that are simultaneously open for extension and yet closed to modification . How can something be both open and closed at once? Martin uses the example of plugin architecture to describe how new features can be added to software without modifying the original source code. Plugins are useful at the system level, but what about at the entity level when objects are interacting with one another? In this case, the key is abstraction. We had a taste of this in the simple examples above when we abstracted out the
logItems functionality of our
collection objects. Let’s see if we can do the same with a slightly more complex program.
In this snippet we use the OLOO pattern to define a
MonsterManager prototype object and two types of monster prototypes,
GreatOldOne. After initializing some monsters and an array of locations, we then initialize a new
myMonsterManager and call its
rampageAll method, unleashing our monsters on those unlucky cities the
randomLocation method happens to choose (sorry!) Can you spot any problems in this code related to OCP adherence?
Take a look at the
rampageAll method — right now it iterates over each monster and checks whether they are of type
GreatOldOne and then logs an appropriate message. What happens when this monster-filled world surfaces some new and terrible type of monster? In order for the program to work we would have to add another branch of conditional logic to the
rampageAll method. In other words, we would have to modify the source code and therefore break the OCP. Doing so would not be a big deal with just one more monster type, but what about 10 new types? Or 20? Or 1,000? (Apparently this poor world is filled with monsters!) In order to extend the behavior of our
MonsterManager (that is, let it deal with more types of monsters) we are going to have to think about how we deal with individual monster types.
MonsterManager probably shouldn’t care about how each different monster rampages, so long as it has the ability to rampage in some fashion. Implementing our program this way would allow us to abstract away the rampage functionality to each individual monster. In other words, we can extend the functionality of the
rampageAll method without changing the source code of
MonsterManager. This use of abstraction is often described as a sort of contract — the objects being used promise to implement some piece of functionality and the object using them promises not to care how they do it. In this case, each monster promises to have a
rampage function and
MonsterManager promises to let them handle the details.
In this snippet, we have a custom
ImplementationError as well as a function called
createWithInterfaceValidation, which takes
interfaceObject parameters. This function iterates over the
interfaceObject parameter to identify which properties should be implemented on the
prototypeObject and throws an
ImplementationError if they are not implemented. If no errors are thrown then the function returns a new object linked to the passed in
prototypeObject. By using this function we can replicate some (though not all) of the functionality of classical interfaces.
In the rest of the snippet we have new version of our
MonsterManager and a few monster types. The difference however is that the
rampageAll function no longer has any conditional logic. Rather, it assumes that each monster has implemented a
rampage function. When creating our monster types we guarantee exactly this by using a
MonsterInterface object as the prototype for each monster type and then using the
createWithInterfaceValidation function whenever we instantiate a new monster. In this fashion, we can be sure that every monster has a valid
rampage method, otherwise an
ImplementationError would be thrown.
This snippet still leaves a lot of room for improvement (DRYer code, type checking, signature checking, custom error messages, additional OCP-adherence opportunities, etc.); however, we can already see a number of improvements over the first version. Most importantly, our
MonsterManager is extensible in that we can add new behavior but it is also closed to modification in that we don’t need to change the source code when adding that new behavior. We can create as many monster types as we like, so long as they all have a
rampage method. This goes to the core of what the OCP is all about.
That’s all for our discussion of the OCP. Stay tuned for articles on the remaining three SOLID principles — starting with part 3 on the Liskov Substitution Principle. And if you want to go back to the beginning of the series, you can find part 1 here. If you have any comments or questions, leave them below — I would love to hear what you think.