SOLID JavaScript: The Open/Closed Principle

On December 19, 2011, in Uncategorized, by derekgreer

This is the second installment in the SOLID JavaScript series which explores the SOLID design principles within the context of the JavaScript language. In this installment, we’ll be covering the Open/Closed Principle.

 

The Open/Closed Principle

The Open/Closed Principle relates to the extensibility of objects. The principle states:

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

By open for extension, this principle means the ability for an entity to be adapted to meet the changing needs of an application. By closed for modification, this principle means that the adaptation of an entity should not result in modification of the entity’s source. More simply, entities which perform varied behavior should be designed to facilitate variability without the need for modification. Adherence to the Open/Closed Principle can help to improve maintainability by minimizing changes made to working code.

To help illustrate, let’s take a look at the following example which dynamically renders questions to a screen based upon the type of answer expected for each question:

 

 
var AnswerType = {
    Choice: 0,
    Input: 1
};

function question(label, answerType, choices) {
    return {
        label: label,
        answerType: answerType,
        choices: choices
    };
}

var view = (function() {
    function renderQuestion(target, question) {
        var questionWrapper = document.createElement('div');
        questionWrapper.className = 'question';

        var questionLabel = document.createElement('div');
        questionLabel.className = 'question-label';
        var label = document.createTextNode(question.label);
        questionLabel.appendChild(label);

        var answer = document.createElement('div');
        answer.className = 'question-input';

        if (question.answerType === AnswerType.Choice) {
            var input = document.createElement('select');
            var len = question.choices.length;
            for (var i = 0; i < len; i++) {
                var option = document.createElement('option');
                option.text = question.choices[i];
                option.value = question.choices[i];
                input.appendChild(option);
            }
        }
        else if (question.answerType === AnswerType.Input) {
            var input = document.createElement('input');
            input.type = 'text';
        }

        answer.appendChild(input);
        questionWrapper.appendChild(questionLabel);
        questionWrapper.appendChild(answer);
        target.appendChild(questionWrapper);
    }

    return {
        render: function(target, questions) {
            for (var i = 0; i < questions.length; i++) {
                renderQuestion(target, questions[i]);
            };
        }
    };
})();

var questions = [
    question('Have you used tobacco products within the last 30 days?', AnswerType.Choice, ['Yes', 'No']),
    question('What medications are you currently using?',AnswerType.Input)
    ];

var questionRegion = document.getElementById('questions');
view.render(questionRegion, questions);

In this example, a view object contains a render method which renders questions based upon each type of question received. A question consists of a label, an answer type (choice or text entry), and an optional list of choices. If the answer type is Answer.Choice, a drop down is created with the options provided. If the answer type is AnswerType.Input, a simple text input is rendered.

Following the pattern already established, adding new input types would require adding new conditions within the render method. This would violate the Open/Closed Principle.

Let’s take a look at an alternate implementation that would allow us to extend the view object’s rendering capabilities without requiring changes to the view object for each new answer type :

 

function questionCreator(spec, my) {
    var that = {};

    my = my || {};
    my.label = spec.label;

    my.renderInput = function() {
        throw "not implemented";
    };

    that.render = function(target) {
        var questionWrapper = document.createElement('div');
        questionWrapper.className = 'question';

        var questionLabel = document.createElement('div');
        questionLabel.className = 'question-label';
        var label = document.createTextNode(spec.label);
        questionLabel.appendChild(label);

        var answer = my.renderInput();

        questionWrapper.appendChild(questionLabel);
        questionWrapper.appendChild(answer);
        return questionWrapper;
    };

    return that;
}

function choiceQuestionCreator(spec) {

    var my = {},
        that = questionCreator(spec, my);

    my.renderInput = function() {
        var input = document.createElement('select');
        var len = spec.choices.length;
        for (var i = 0; i < len; i++) {
            var option = document.createElement('option');
            option.text = spec.choices[i];
            option.value = spec.choices[i];
            input.appendChild(option);
        }

        return input;
    };

    return that;
}

function inputQuestionCreator(spec) {

    var my = {},
        that = questionCreator(spec, my);

    my.renderInput = function() {
        var input = document.createElement('input');
        input.type = 'text';
        return input;
    };

    return that;
}

var view = {
    render: function(target, questions) {
        for (var i = 0; i < questions.length; i++) {
            target.appendChild(questions[i].render());
        }
    }
};

var questions = [
    choiceQuestionCreator({
    label: 'Have you used tobacco products within the last 30 days?',
    choices: ['Yes', 'No']
}),
    inputQuestionCreator({
    label: 'What medications are you currently using?'
})
    ];

var questionRegion = document.getElementById('questions');

view.render(questionRegion, questions);

There’s a few techniques being used here, so let’s walk through them one at a time.

First, we’ve factored out the code responsible for creating questions into a functional constructor named questionCreator. This constructor utilizes the Template Method Pattern for delegating the creation of each answer to extending types.

Second, we’ve replaced the use of the former constructor’s properties  with a private spec property  which serves as the questionCreator constructor’s interface. Since we’re encapsulating the rendering behavior with the data it operates upon, we no longer need these properties to be publicly accessible.

Third, we’ve identified the code which creates each answer type as a family of algorithms and factored out each algorithm into a separate object (a technique referred to as the the Strategy Pattern) which extends the questionCreator object using differential inheritance.

As an added benefit to this refactoring, we were able to eliminate the need for an AnswerType enumeration and we were able make the choices array a requirement specific to the choiceQuestionCreator interface.

The refactored version of the view object can now be cleanly extended by simply extending new questionCreator objects.

Next time, we’ll discuss the third principle in the SOLID acronym: The Liskov Substitution Principle.

Tagged with:  
  • Chad Myer

    there is circular dependency between choiceQuestionCreator and questionCreator

    • derekgreer

      The questionCreator doesn’t reference the choiceQuestionCreator. Can you elaborate on the problem your seeing?

  • John

    Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. We should be able to extend software entities without actually modifying them. This is a crucial principle of SOLID and continuous integration. We usually apply this principle to OOP and we should apply it in every day javascript programming as well. The following video starting from the definition of this principle, identifies some common code smell and puts it into practice with a javascript example

    https://youtu.be/t7RgyY9OOd0

  • Greg

    When you’re thinking about these problems, do you start with what you want the question array to look like? You know that if the array is made up of calls to functions/constructors that are specific to the question type, then adding a new question type will mean simply adding a new function (which is good).

    And then work from there?

    • derekgreer

      Hey, Greg. This is often evolutionary. At times, I might find myself designing some component which I know is going to benefit from the use of a strategy pattern, but probably more often I’d arrive at such a design by implementing some component that handles an initial case and then down the road factoring out certain implementation details once there’s a need to do the same thing in a different way. For example, say you work for a company that serves as a store front for other vendors. You might create a PaymentService that processes credit cards with payment gateway X and then 6 months down the road have a requirement to process payments with a payment gateway specific to each vendor. In this case, you might factor out the details of how to process a credit card with payment gateway X into some strategy and then create additional strategies for each new type of payment gateway supported.The PaymentService would retain common logic like if the credit card is declined then do X or if the charge is accepted then do Y, but the details of dealing with each gateway would be factored out.

      • Greg

        I refactor too, but always seem to end up with procedural-looking code, not the fancy sort of wrapper/factory/strategy code that you ended up with here with functions making objects using callbacks…

        Sounds like maybe your refactoring process just has a few more tools?

  • Pingback: Principios SOLID con JavaScript: El principio abierto/cerrado (Traducción del Inglés) – Adolfo Sanz De Diego()