SOLID принципы в JavaScript. Принцип открытости-закрытости
Принципы хорошего кода

SOLID принципы в JavaScript. Принцип открытости-закрытости

Во второй статье о SOLID-принципах в JavaScript мы рассмотрим второй SOLID-принцип — принцип открытости-закрытости. Данный принцип также был описан в книге Роберта Мартина Быстрая разработка программ. Принципы, примеры, практика, а впервые сформулирован Бертраном Мейером в 1988 году.

Суть принципа

Принцип открытости-закрытости звучит следующим образом:

«Программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения».

Т.е., архитектура приложения должна позволять менять поведение неких сущностей без изменения их исходного кода.

Этот принцип особенно важен для больших бизнес-приложений, когда требования изменяются, функционал расширяется или изменяется, что может повлечь массу неожиданных изменений. А каскад изменений влечет за собой в свою очередь трудозатраты на тестирование и неприятные непредсказуемые ошибки.

Принцип открытости-закрытости в JavaScript

Чтобы проиллюстрировать принцип открытости-закрытости в JavaScript рассмотрим пример, реализующий простой опрос:


/**
 * Типы вопросов (объект, содержащий возможные типы вопросов)
 */
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 wrapper = $('<div class="question"></div>'),
            label   = $('<div class="question-label"></div>'),
            answer  = $('<div class="question-input"></div>');

        label.html(question.label);

        if (question.answerType === AnswerType.Choice) {
            var input = $('<select></select>');
            var len = question.choices.length;
            for (var i = 0; i < len; i++) {
                var option = $('<option></option>');
                option.text(question.choices[i]);
                option.val(question.choices[i]);
                input.append(option);
            }
        }
        else if (question.answerType === AnswerType.Input) {
            var input = $('<input type="text" />');
        }

        answer.append(input);
        wrapper.append(label);
        wrapper.append(answer);
        target.append(wrapper);
    }

    return {
        /**
         * Функция отображения списка вопросов
         */
        render: function(target, questions) {
            for (var i = 0; i < questions.length; i++) {
                renderQuestion(target, questions[i]);
            };
        }
    };
})();

var questions = [
    question('Ваш возраст', AnswerType.Input),
    question('Любите ли вы фантастическую литературу?', AnswerType.Choice, ['Да', 'Нет']),
    question('Сколько часов в неделю в среднем вы тратите на чтение художественной литературы?', AnswerType.Choice, ['менее часа', '2-5 часов', '5-8 часов', '9-15 часов', 'более 15 часов'])
];

var questionContainer = $('#questions');
view.render(questionContainer, questions);

В примере выше объект представления имеет метод render(), который принимает объект-контейнер и массив вопросов и по очереди отображает каждый вопрос. Вроде бы все логично, однако добавление нового типа вопроса требует изменения метода render(), что противоречит принципу открытости-закрытости.

Рассмотрим теперь другой пример, где этот момент исправлен:


/**
 * Абстрактный объект, отображающий вопрос
 */
function questionCreator(params) {
    var that = {};

    that.renderInput = function() {
        throw "Not implemented";
    };

    that.render = function(target) {
        var wrapper = $('<div class="question"></div>'),
            label   = $('<div class="question-label"></div>'),
            answer;

        label.html(params.label);
        answer = that.renderInput();

        wrapper.append(label);
        wrapper.append(answer);

        return wrapper;
    };

    return that;
}

/**
 * Объект, отображающий вопрос с типом ответа - выпадающий список
 */
function choiceQuestionCreator(params) {
    var that = questionCreator(params);

    that.renderInput = function() {
        var input = $('<select></select>');
        var len = params.choices.length;
        for (var i = 0; i < len; i++) {
            var option = $('<option></option>');
            option.text(params.choices[i]);
            option.val(params.choices[i]);
            input.append(option);
        }

        return input;
    };

    return that;
}

/**
 * Объект, отображающий вопрос с типом ответа - поле ввода
 */
function inputQuestionCreator(params) {
    var that = questionCreator(params);

    that.renderInput = function() {
        var input = $('<input type="text" />');
        return input;
    };

    return that;
}

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

var questions = [
    inputQuestionCreator({
        label: 'Ваш возраст'
    }),
    choiceQuestionCreator({
        label: 'Любите ли вы фантастическую литературу?',
        choices: ['Да', 'Нет']
    }),
    choiceQuestionCreator({
        label: 'Сколько часов в неделю в среднем вы тратите на чтение художественной литературы?',
        choices: ['менее часа', '2-5 часов', '5-8 часов', '9-15 часов', 'более 15 часов']
    })    
];

var questionContainer = $('#questions');
view.render(questionContainer, questions);

В примере выше мы избавились от необходимости изменять метод render представления для добавления нового типа ответа. Ответственность за создание вопросов лежит теперь на «наследниках» абстрактного объекта questionCreator. Каждый из наследников реализует свой метод renderInput, а также инкапсулирует данные, необходимые данному типу поля.

Для создания нового типа вопроса теперь необходимо добавить объект, расширяющий questionCreator, и реализовать у него свой метод renderInput. Также это позволило нам избавиться отперечисления типов вопросов AnswerType, которое использовалось в первом варианте.

Заключение

В данной статье мы рассмотрели второй принцип объектно-ориентированного программирования — принцип открытости-закрытости — в рамках языка JavaScript. Выполнение этого принципа позволяет сделать систему более устойчивой к изменениям, гибкой, легко масштабируемой.

В одной из предыдущих статей мы уже рассматривали первый принцип SOLID - принцип единственной ответственности в JavaScript.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться