Connecting...

W1siziisimnvbxbpbgvkx3rozw1lx2fzc2v0cy9zawduawz5lxrly2hub2xvz3kvanbnl2jhbm5lci1kzwzhdwx0lmpwzyjdxq

Maintainable Code and the Open-Closed Principle by Severin Perez

W1siziisijiwmtkvmdevmtevmdkvndcvmzyvodg0l3blegvscy1wag90by0xmdm2odu3lmpwzwcixsxbinailcj0ahvtyiisijkwmhg5mdbcdtawm2uixv0

Explore the Open Closed Principle and how exactly you can put it into practice. In this article by Severin Perez learn how to conduct your exploration in JavaScript, it might not be the most associated language with the Open Closed Principle tools but Severin gives it a go!


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:

  1. The Single Responsibility Principle — Classes should have a single responsibility and thus only a single reason to change.
  2. The Open/Closed Principle — Classes and other entities should be open for extension but closed for modification.
  3. The Liskov Substitution Principle — Objects should be replaceable by their subtypes.
  4. The Interface Segregation Principle — Interfaces should be client specific rather than general.
  5. 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:

1 function announce(collection) {
2   console.log(collection.description);
3  
4   collection.items.forEach(function(element) {
5     console.log(element);
6   });
7 }
8
9 var favoriteCities = {
10   items: ["Copenhagen", "Kampala", "Montevideo"],
11  
12   description: "My favorite cities around the world:",
13 };
14
15 announce(favoriteCities);
16   // Logs: 
17   //   "My favorite cities around the world:"
18   //   "Copenhagen"
19   //   "Kampala"
20   //   "Montevideo"

In this snippet we have a simple function called 'announce' that takes an object as an argument and uses that object’s 'items' and '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?

1 function announce(collection) {
2   console.log(collection.description);
3  
4   collection.items.forEach(function(element) {
5     console.log(element);
6   });
7 }
8
9 var favoriteCities = {
10   items: {
11     "Denmark": "Copenhagen",
12     "Uganda": "Kampala",
13     "Uraguay": "Montevideo"
14   },
15   
16   description: "My favorite cities around the world:",
17 };
18
19 announce(favoriteCities);
20   // TypeError: collection.items.forEach is not a function

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 'items'.

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:

1 function announce(collection) {
2   console.log(collection.description);
3  
4   collection.logItems();
5 }
6
7 var favoriteCities = {
8   items: {
9     "Denmark": "Copenhagen",
10     "Uganda": "Kampala",
11     "Uraguay": "Montevideo"
12   },
13   
14   description: "My favorite cities around the world:",
15  
16   logItems: function() {
17     Object.keys(this.items).forEach(function(key) {
18       console.log(this.items[key]);
19     }, this);
20   },
21 };
22
23 announce(favoriteCities);
24  // Logs: 
25  //   "My favorite cities around the world:"
26  //   "Copenhagen"
27  //   "Kampala"
28  //   "Montevideo"

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 [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 our 'collection' objects. Let’s see if we can do the same with a slightly more complex program.

1 // Monster Types and Manager
2 var MonsterManager = {
3   init: function(monsters, locations) {
4     this.monsters = monsters;
5     this.locations = locations;
6   },
7  
8   getRandomLocation: function() {
9     function getRandomInt(max) {
10       return Math.floor(Math.random() * Math.floor(max));
11     }
12     
13     return this.locations[getRandomInt(this.locations.length)];
14   },
15  
16   rampageAll: function() {
17     this.monsters.forEach(function(monster) {
18       var location = this.getRandomLocation();
19       
20       if (Object.getPrototypeOf(monster) == Kaiju) {
21         console.log(
22           "The " + monster.type + " " + monster.name + 
23           " is rampaging through " + location + "!"
24         );
25       } else if (Object.getPrototypeOf(monster) == GreatOldOne) {
26         console.log(
27           "The " + monster.type + " " + monster.name +
28           " has awaken from its slumber in " + location + "!"
29         );
30       }
31     }, this);
32   }
33 };
34
35 var Kaiju = {
36   init: function(name) {
37     this.name = name;
38     this.type = "Kaiju";
39     
40     return this;
41   }
42 };
43
44 var GreatOldOne = {
45   init: function(name) {
46     this.name = name;
47     this.type = "Great Old One";
48    
49     return this;
50   }
51 };
52
53 // Rampage!
54 var monsters = [];
55 var locations = ["Athens", "Budapest", "New York", "Santiago", "Tokyo"];
56
57 var rodan = Object.create(Kaiju).init("Rodan");
58 monsters.push(rodan);
59
60 var gzxtyos = Object.create(GreatOldOne).init("Gzxtyos");
61 monsters.push(gzxtyos);
62
63 var myMonsterManager = Object.create(MonsterManager);
64 myMonsterManager.init(monsters, locations);
65
66 myMonsterManager.rampageAll();
67   // Logs: (with variable city names)
68     // The Kaiju Rodan is rampaging through Santiago!
69     // The Great Old One Gzxtyos has awaken from its slumber in Athens!

In this snippet we use the OLOO pattern to define a 'MonsterManager' prototype object and two types of monster prototypes, 'Kaiju' and 'GreatOldOne'. After initializing some monsters and an array of locations, we then initialize a new 'MonsterManager' called '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 'Kaiju' or '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.

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 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.

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.

1 // Interface Approximation Utilities
2 function ImplementationError(message) {
3   this.name = "ImplementationError";
4   this.message = message;
5 }
6 ImplementationError.prototype = new Error();
7
8 function createWithInterfaceValidation(prototypeObject, interfaceObject) {
9   Object.keys(interfaceObject).forEach(function(key) {
10     if (prototypeObject[key] === null || typeof prototypeObject[key] !== "function") {
11       throw new ImplementationError(
12         "Required method " + key + " has not been implemented."
13       );
14     }
15   });
16  
17   return Object.create(prototypeObject);
18 }
19
20 // Monster Types and Manager
21 var MonsterManager = {
22   init: function(monsters, locations) {
23     this.monsters = monsters;
24     this.locations = locations;
25   },
26  
27   getRandomLocation: function() {
28     function getRandomInt(max) {
29       return Math.floor(Math.random() * Math.floor(max));
30     }
31    
32     return this.locations[getRandomInt(this.locations.length)];
33   },
34   
35   rampageAll: function() {
36     this.monsters.forEach(function(monster) {
37       var location = this.getRandomLocation();
38      
39       monster.rampage(location);
40     }, this);
41   }
42 };
43 
44 var MonsterInterface = {
45   init: null,
46   rampage: null,
47 };
48
49 var Kaiju = Object.create(MonsterInterface);
50 Kaiju.init = function(name) {
51   this.name = name;
52   this.type = "Kaiju";
53  
54   return this;
55 };
56 Kaiju.rampage = function(location) {
57   console.log(
58     "The " + this.type + " " + this.name + 
59     " is rampaging through " + location + "!"
60   );
61 };
62
63 var GreatOldOne = Object.create(MonsterInterface);
64 GreatOldOne.init = function(name) {
65   this.name = name;
66   this.type = "Great Old One";
67  
68   return this;
69 };
70 GreatOldOne.rampage = function(location) {
71   console.log(
72     "The " + this.type + " " + this.name +
73     " has awaken from its slumber in " + location + "!"
74   );
75 };
76
77 var MythicalMonster = Object.create(MonsterInterface);
78 MythicalMonster.init = function(name) {
79   this.name = name;
80   this.type = "Mythical Monster";
81   
82   return this;
83 };
84 MythicalMonster.rampage = function(location) {
85   console.log(
86     "The " + this.type + " " + this.name +
87     " has been sighted in " + location + "!"
88   );
89 };
90 
91 // Rampage!
92 var monsters = [];
93 var locations = ["Athens", "Budapest", "New York", "Santiago", "Tokyo"];
94
95 var rodan = createWithInterfaceValidation(Kaiju, MonsterInterface);
96 rodan.init("Rodan");
97 monsters.push(rodan);
98
99 var gzxtyos = createWithInterfaceValidation(GreatOldOne, MonsterInterface);
100 gzxtyos.init("Gzxtyos");
101 monsters.push(gzxtyos);
102
103 var cerberus = createWithInterfaceValidation(MythicalMonster, MonsterInterface);
104 cerberus.init("Cerberus");
105 monsters.push(cerberus);
106
107 var myMonsterManager = Object.create(MonsterManager);
108 myMonsterManager.init(monsters, locations);
109
110 myMonsterManager.rampageAll();
111   // Logs: (with variable city names)
112     // The Kaiju Rodan is rampaging through Tokyo!
113     // The Great Old One Gzxtyos has awaken from its slumber in Athens!
114     // The Mythical Monster Cerberus has been sighted in New York!

In this snippet, we have a custom 'ImplementationError' as well as a function called 'createWithInterfaceValidation', which takes 'prototypeObject' and '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.



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.'


References

  1. Paper: The Open-Closed Principle; Martin, Robert C.; 1996
  2. Article: The Open Closed Principle; Martin, Robert C.; 2014
  3. Resource: clean-code-javascript; Mcdermott, Ryan
  4. Wikipedia: Open-closed principle


This article was written by Severin Perez and originally posted on Medium