Maintainable Code and the Open-Closed Principle
Abstraction and the Open-Closed Principle in JavaScript
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).
In this post, we’re going to explore the OCP and how to put it into practice. In a slight departure from many discussions on the OCP, we’re going to conduct our exploration in JavaScript — a language not often associated with classical OCP tools such as interfaces. However, JavaScript code is as deserving of SOLID adherence as any other code, so let’s give it a shot!
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 [1]. 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’sitems
anddescription
properties to log a message to the console. When we call this function and pass it thefavoriteCities
object we get the expected output. But what if we decide that we don’t want thefavoriteCities
object to store itsitems
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 ourannounce
function. The reason is that theannounce
function knows too much about howfavoriteCities
was implemented and expects it to have anitems
property that is an array. Fixing this would be relatively trivial (perhaps we could add a conditional to theannounce
function to check first whether thecollection.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 usedcollection.items
? We would then have to add conditionals to every place that referenceditems
.A better solution is to use polymorphism and to let each
collection
object decide for itself how itsitems
should be iterated over and logged. In this pattern, theannounce
function doesn’t care whether the collections it works with use arrays, objects, or some other data structure to hold theiritems
. Here is one approach:In this final snippet, we provide
favoriteCities
with alogItems
method that implements how to log its items. As far asannounce
is concerned, it can deal with any collection object so long as it has adescription
property and alogItems
method. This is the OCP in action — theannounce
function is extensible because it can handle anycollection
that guarantees these two properties but it is also closed to modification because we don’t have to change the source code inannounce
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 [2]. 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 ourcollection
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,Kaiju
andGreatOldOne
. After initializing some monsters and an array of locations, we then initialize a newMonsterManager
calledmyMonsterManager
and call itsrampageAll
method, unleashing our monsters on those unlucky cities therandomLocation
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 typeKaiju
orGreatOldOne
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 therampageAll
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 ourMonsterManager
(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.Ultimately, the
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 therampageAll
method without changing the source code ofMonsterManager
. 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 arampage
function andMonsterManager
promises to let them handle the details.As a means of implementing this pattern, languages like C# and Java have an abstraction called an interface. An interface can be used to create the kind of contracts described above. Unfortunately, JavaScript does not have interfaces; however, we can roughly approximate some of the behavior of an interface by using prototypal delegation and a custom validation function. Let’s try to do that with our monster program.
In this snippet, we have a custom
ImplementationError
as well as a function calledcreateWithInterfaceValidation
, which takesprototypeObject
andinterfaceObject
parameters. This function iterates over theinterfaceObject
parameter to identify which properties should be implemented on theprototypeObject
and throws anImplementationError
if they are not implemented. If no errors are thrown then the function returns a new object linked to the passed inprototypeObject
. 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 therampageAll
function no longer has any conditional logic. Rather, it assumes that each monster has implemented arampage
function. When creating our monster types we guarantee exactly this by using aMonsterInterface
object as the prototype for each monster type and then using thecreateWithInterfaceValidation
function whenever we instantiate a new monster. In this fashion, we can be sure that every monster has a validrampage
method, otherwise anImplementationError
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 arampage
method. This goes to the core of what the OCP is all about.TL;DR
The second of the SOLID principles of software development is the Open-Closed Principle (OCP), which says that software entities (objects, classes, modules, etc.) should be “open for extension” but “closed to modification”. In this context, extension means adding new behavior and modification means altering existing source code. The OCP is a useful principle for keeping your code maintainable because it ensures that old working code is not changed (causing downstream breakage) while simultaneously allowing for the addition of new behavior. One method for adhering to the OCP is relying on abstractions rather than concretions. When one object interacts with another, it should do so through an abstraction, allowing its partner object to worry about specific implementation. The classic way to do this is with interfaces or other abstractions; however, some languages like JavaScript do not provide native interface abstractions. In this case, it is still possible to follow the OCP either through convention or through custom validation methods.
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.
If you would like alerts when a new article is published you can follow me here on Medium, on Twitter, or subscribe on my personal blog where these articles are cross-published. Happy coding!
Maintainable Code and the Open-Closed Principle – Severin Perez – Medium
Pages: 1 2