A critical aspect in the success or failure of a Software Product is its ability and flexibility to change when new requirements arrive. A system which components are not complicated to extend, grow, or adapt, and when doing so, new changes do not bring about chaos, will probably live way longer and will have better chances of success. We have all seen that codebase that everyone is afraid of touching, because no one knows what is going to break, or because it would take too long to accomplish a seemingly simple thing.

There all well known patterns, principles and practices that will help any developer be a better professional and build better software. Almost everyone in our field has heard about SOLID (https://en.wikipedia.org/wiki/SOLID), which are a set of principles that help you do exactly that, be a better professional and build better software. Although what I have seen in practice, and some software developers with interviewing experience can probably attest to, is that a great deal of developers have an idea about what they are, but do not really understand, or practice some or most of them. Today, I want to talk about the second of its principles, the O in SOLID, the Open/Closed Principle. I want to show a common bad practice that violates this principle, and what I do to avoid that trap, and that I try to refactor to, when convenient.

I have to say that I did not come up with the idea, I learned it from Jimmy Bogard (creator of AutoMapper and MediatR), in a presentation he gave that if I remember correctly, was about strengthening weak Business Domains. I have used this technique ever since, which he implemented in C# (using Polymorphism), but this article is about sharing with you the way I do it in JavaScript, which by the way does not use ECMAScript 6 “classes”, but a simple factory function.

Initial requirement:

You need to create a function that takes an argument, which possible values are ‘even’ and ‘odd’, and return an array with the even or odd numbers (depending on the value of the argument) that are between 0 and 10 (both ends exclusive). The limit, 10, will be fixed for simplicity sake, since our goal with this exercise is not algorithm creation but code design.

Common solution:

const getSequence = type => {

if (type === 'even') {

return getEvenNumbers();

} else if (type === 'odd') {

return getOddNumbers();

} else {

throw Error(`Invalid argument value ${type}`);

}

};

We have not implemented getEvenNumbers or getOddNumbers yet, and honestly, we do not need them to follow along, although they will be included for completion sake. Besides that, it looks like very normal code, with if/else-if/else statements that cover the possible valid scenarios (we are not really interested in edge cases here for the purpose of the article).

A variation of above code would be:

const getSequence = type => {

switch(type) {

case 'even':

return getEvenNumbers();

case 'odd':

return getOddNumbers();

default:

throw Error(`Invalid argument value ${type}`);

}

};

In this case, a switch statement was used to accomplish the same result. It does not matter which of these two approaches you use, the problems they carry are the same, bear with me.

Since the implementation for getEvenNumbers has a lot in common with the one for getOddNumbers, I have refactored out the common pieces into a single function called includeIfConditionIsMet, you can see their implementations below:

const getEvenNumbers = limit => includeIfConditionIsMet(limit, number => number % 2 === 0);



const getOddNumbers = limit => includeIfConditionIsMet(limit, number => number % 2 !== 0);



const includeIfConditionIsMet = (limit = 10, predicate) => {

return (function inner(array, number) {

if (number === limit) {

return array;

} return inner(predicate(number) ? [...array, number] : array, number + 1);

})([], 1);

};

This last function uses a default limit of 10 and a predicate (only thing that varies between getEvenNumbers and getOddNumbers), to build and return the desired array. As you know, this last function could have been implemented in a different number of ways. Since here we do not really care about its implementation details, I just did it using recursion, just because I do not like for, foreach, while, or for of loops (I do not like imperative programming, you may find the “why” in one of my other articles https://itnext.io/a-more-declarative-solution-to-the-t9-problem-in-both-javascript-and-c-13ab7f7a859b), and I am not really worried about eating up all my stack frames in this sample problem (yes, I am aware that Tail Call Optimization did not make it to ECMAScript at the end).

The second requirement:

You are very happy with your code, and the way it works. Now your boss asks you to add another functionality to your program, where given the same input, ‘even’ or ‘odd’, and an array of integer numbers, you need to return a new array that contains the elements in the even or odd positions (depending on the value of the argument).

Let’s stick to the if/else solution for the first and second “requirement”. You are certain you are going to impress the boss when he sees how fast you are done. Now your code looks like this:

const getElementsByPositionType = (type, elements) => {

if (type === 'even') {

return getElementsInEvenPosition(elements);

} else if (type === 'odd') {

return getElementsInOddPosition(elements);

} else {

throw Error(`Invalid argument value ${type}`);

}

};

For completion sake, the implementation for getElementsInEvenPosition included, it is going to be very easy for you to come up with getElementsInOddPosition’s.

const getElementsInEvenPosition = elements => elements.reduce((a, c, i) => i % 2 === 0 ? a.concat(c) : a, []);

But reality is, your Boss sees a problem right away during a code review session. There seems to be a lot of duplication between the function getSequence and getElementsByPositionType. The if/else-if/else check clauses are identical, and now he is pretty sure you copied and pasted the first one and just made modifications to it (God knows I did). He is wondering if every time there is a new “requirement” like this one, you are going to do the same, and he lets you know about his concern.

The problem (part I):

Duplication is not a good thing, it brings about a lot of problems that I am sure you are aware of. Trust me when I tell you, I have seen codebases with 15 plus if/else or switch statements checking for the same range of possible values. The most typical case that I have encountered is related to enums in C#. For example, a Person class that has a Type property, which is an enum with the values, Manager, VP, etc.; and there is a different logic to calculate something for each of those, like a bonus pay, but they also differ from each other in several other operations, so for each different type-based calculation, there is a switch statement based on the enum Type property. When the day comes that a new value needs to be added to the enum, the poor soul that has to do it has to check the entire codebase looking for those switch statements and add a new case clause to each of those. And do not miss one, because it is going to be your fault, you will be the one being hung, not the one who created the enum, or the army of soldiers that added to the switch/case/default hell.

But so far you have not had that problem, you just have a duplication problem…just wait for it.

The third requirement:

You still have not recovered from your programmer’s ego going two or three steps down after your boss did not look very happy with the way you solved the last “requirement”; and yet, he has a new task for you to take on. “Our product needs to support similar functionalities for prime numbers”, is the phrase you cannot get out of your head during that nice first date night you are having, because she/he might not see you again, but your boss will.

During your first try, you come up with something, you looked for all those places in code where you needed to add the prime numbers logic, and modified the code, to look like this:

const getSequence = type => {

if (type === 'even') {

return getEvenNumbers();

} else if (type === 'odd') {

return getOddNumbers();

} else if (type === 'prime') {

return getPrimeNumbers();

} else {

throw Error(`Invalid argument value ${type}`);

}

};



const getElementsByPositionType = (type, elements) => {

if (type === 'even') {

return getElementsInEvenPosition(elements);

} else if (type === 'odd') {

return getElementsInOddPosition(elements);

} else if (type === 'prime') {

return getElementsInPrimePosition();

} else {

throw Error(`Invalid argument value ${type}`);

}

};

Good thing there were only two places to go to, you might not be that lucky two years down the road, when the project is way bigger and complex.

You can implement getElementsInPrimePosition as an exercise.

Something in your heart tells you that you must not show this to your boss, you might not have enough money to pay for the next date night if you do (I know you did well the first night, and he/she will see you again).

The problem (part II):

You first violated the DRY Principle https://en.wikipedia.org/wiki/Don%27t_repeat_yourself, and this time, you are violating the Open/Closed Principle https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle. This last one basically states, “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”. You did open surgery on those two places and modified the code. Anytime you modify code like that, specially in many different places, you take a huge risk of breaking things, and you also create code that is harder to follow, maintain and extend. Do not be selfish, think of that poor soul that will come to that code after you, which might as well be your own, next year, next month or 2 hours later.

If you want to learn way more about the Open/Closed Principle and in general the SOLID principles, I recommend you read Agile Principles, Patterns, and Practices in C# by Robert C. Martin.

The solution:

You do not know any better, but you do have the phone number of a JavaScript developer you trust, and that is going to help you out. She tells you, I would have started like this.

const asEnumeration = dictionary => {

return Object.freeze({

fromValue: value => {

if (dictionary[value]) {

return dictionary[value];

}

throw Error(`Invalid enumeration value ${value}`);

}

});

};

I have used Enumeration as part of this function name, to honor the name Jimmy Bogard gave to his abstract class, which is the cornerstone in his C# design aimed at solving all above aforementioned problems.

Anyway, this function is going to take a dictionary-like object as a parameter, and it is going to return an immutable object with a single method, fromValue (also named in honor to Jimmy’s Enumeration class). That way, you can create enumeration-like objects based on the passed in dictionary.

const numbersEnumeration = asEnumeration({

'even': {

getSequence: getEvenNumbers

},

'odd': {

getSequence: getOddNumbers

}

});

Now I can have a numbers Enumeration, which keys are the possible values of our business domain, ‘even’, ‘odd’ and one day ‘prime’ or whatever the case may be. Then I can implement getSequence like this:

const getSequence = type => numbersEnumeration.fromValue(type).getSequence(10);

Then when the new “requirement” for the “elements by position type” came along, instead of repeating yourself in two or maybe 15 different places, you would have just added, in one place, a new property to each object in the dictionary, like this:

const numbersEnumeration = asEnumeration({

'even': {

getSequence: getEvenNumbers,

getElementsByPositionType: getElementsInEvenPosition,

},

'odd': {

getSequence: getOddNumbers,

getElementsByPositionType: getElementsInOddPosition,

}

});

Then getElementsByPositionType would have been:

const getElementsByPositionType = (type, elements) => numbersEnumeration.fromValue(type).getElementsByPositionType(elements);

You can refactor the code and call getSequence and getElementsInPrimePosition like this:

const fromValue = type => numbersEnumeration.fromValue(type);



const getSequence = type => fromValue(type).getSequence(10);



const getElementsByPositionType = (type, elements) => fromValue(type).getElementsByPositionType(elements);



console.log(getSequence('even'));

console.log(getElementsByPositionType('odd', [6, 7, 8, 9, 10]));

Now when the prime numbers logic comes along, or any other logic for that matter, you just add a new object to the mapping dictionary (changes in only one place again) like this:

const numbersEnumeration = asEnumeration({

'even': {

getSequence: getEvenNumbers,

getElementsByPositionType: getElementsInEvenPosition,

},

'odd': {

getSequence: getOddNumbers,

getElementsByPositionType: getElementsInOddPosition,

},



'prime': {

getSequence: getPrimeNumbers,

getElementsByPositionType: getElementsInPrimePosition,

}

});

There are no more if/else/switch statements to chase down and change throughout your codebase. I have to admit, there is no code that is fully closed to modification, so do not take that literally, or feel bad if you find yourself modifying existing code while trying to not violate this principle, there is no way to add or change features in a system without modifying code. But it is for sure a better practice to do so by adding or changing code in one, instead of multiple places, and preferably adding/extending than altering.

I also have to say that this technique is not meant to replace all if/else/switch statements, but to replace those that work on a fixed set of values like those enums types contain, and that tend to create a lot of duplication around them. I have used this technique for years now (I started first doing it in C#, and then in JavaScript), and it has never let me down.

Whether you are coding in a language like C#, through classes, inheritance and polymorphism (or even better, composition https://en.wikipedia.org/wiki/Composition_over_inheritance) or in a language like JavaScript (using factory functions, objects and even object composition), if you want to create a strong domain (which in my opinion is the exact opposite of an anemic domain https://www.martinfowler.com/bliki/AnemicDomainModel.html) enum-like types should have behavior, instead of just being “primitive types”.

Happy Coding!