鍍金池/ 教程/ HTML/ S.O.L.I.D 五大原則之開閉原則 OCP
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計模式之狀態(tài)模式
JavaScript 核心(晉級高手必讀篇)
設(shè)計模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計模式之中介者模式
設(shè)計模式之裝飾者模式
設(shè)計模式之模板方法
設(shè)計模式之外觀模式
強大的原型和原型鏈
設(shè)計模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達(dá)式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計模式之適配器模式
設(shè)計模式之組合模式
設(shè)計模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫高質(zhì)量 JavaScript 代碼的基本要點
求值策略
閉包(Closures)
對象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計模式之代理模式
變量對象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴倒置原則 DIP
設(shè)計模式之迭代器模式
立即調(diào)用的函數(shù)表達(dá)式
設(shè)計模式之享元模式
設(shè)計模式之原型模式
根本沒有“JSON 對象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實現(xiàn)
全面解析 Module 模式
對象創(chuàng)建模式(下篇)
設(shè)計模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開閉原則 OCP
設(shè)計模式之橋接模式
設(shè)計模式之策略模式
設(shè)計模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計模式之工廠模式

S.O.L.I.D 五大原則之開閉原則 OCP

前言

本章我們要講解的是 S.O.L.I.D 五大原則 JavaScript 語言實現(xiàn)的第 2 篇,開閉原則 OCP(The Open/Closed Principle )。

開閉原則的描述是:

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
軟件實體(類,模塊,方法等等)應(yīng)當(dāng)對擴展開放,對修改關(guān)閉,即軟件實體應(yīng)當(dāng)在不修改的前提下擴展。

open for extension(對擴展開放)的意思是說當(dāng)新需求出現(xiàn)的時候,可以通過擴展現(xiàn)有模型達(dá)到目的。而 Close for modification(對修改關(guān)閉)的意思是說不允許對該實體做任何修改,說白了,就是這些需要執(zhí)行多樣行為的實體應(yīng)該設(shè)計成不需要修改就可以實現(xiàn)各種的變化,堅持開閉原則有利于用最少的代碼進(jìn)行項目維護(hù)。

問題代碼

為了直觀地描述,我們來舉個例子演示一下,下屬代碼是動態(tài)展示 question 列表的代碼(沒有使用開閉原則)。

// 問題類型
var AnswerType = {
    Choice: 0,
    Input: 1
};
// 問題實體
function question(label, answerType, choices) {
    return {
        label: label,
        answerType: answerType,
        choices: choices // 這里的choices是可選參數(shù)
    };
}
var view = (function () {
    // render一個問題
    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';
        // 根據(jù)不同的類型展示不同的代碼:分別是下拉菜單和輸入框兩種
        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 {
        // 遍歷所有的問題列表進(jìn)行展示
        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);

上面的代碼,view 對象里包含一個 render 方法用來展示 question 列表,展示的時候根據(jù)不同的 question 類型使用不同的展示方式,一個 question 包含一個 label 和一個問題類型以及 choices 的選項(如果是選擇類型的話)。如果問題類型是 Choice 那就根據(jù)選項生產(chǎn)一個下拉菜單,如果類型是 Input,那就簡單地展示 input輸入框。

該代碼有一個限制,就是如果再增加一個 question 類型的話,那就需要再次修改 renderQuestion 里的條件語句,這明顯違反了開閉原則。

重構(gòu)代碼

讓我們來重構(gòu)一下這個代碼,以便在出現(xiàn)新 question 類型的情況下允許擴展 view 對象的 render 能力,而不需要修改 view 對象內(nèi)部的代碼。

先來創(chuàng)建一個通用的 questionCreator 函數(shù):

function questionCreator(spec, my) {
    var that = {};
    my = my || {};
    my.label = spec.label;
    my.renderInput = function () {
        throw "not implemented"; 
        // 這里renderInput沒有實現(xiàn),主要目的是讓各自問題類型的實現(xiàn)代碼去覆蓋整個方法
    };
    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();
        // 該render方法是同樣的粗合理代碼
        // 唯一的不同就是上面的一句my.renderInput()
        // 因為不同的問題類型有不同的實現(xiàn)
        questionWrapper.appendChild(questionLabel);
        questionWrapper.appendChild(answer);
        return questionWrapper;
    };
    return that;
}

該代碼的作用組合要是 render 一個問題,同時提供一個未實現(xiàn)的 renderInput 方法以便其他 function 可以覆蓋,以使用不同的問題類型,我們繼續(xù)看一下每個問題類型的實現(xiàn)代碼:

function choiceQuestionCreator(spec) {
    var my = {},
that = questionCreator(spec, my);           
    // choice類型的renderInput實現(xiàn)
    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);
    // input類型的renderInput實現(xiàn)
    my.renderInput = function () {
        var input = document.createElement('input');
        input.type = 'text';
        return input;
    };
    return that;
}

choiceQuestionCreator 函數(shù)和 inputQuestionCreator 函數(shù)分別對應(yīng)下拉菜單和 input 輸入框的 renderInput 實現(xiàn),通過內(nèi)部調(diào)用統(tǒng)一的 questionCreator(spec, my)然后返回 that 對象(同一類型哦)。

view 對象的代碼就很固定了。

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

所以我們聲明問題的時候只需要這樣做,就 OK 了:

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);

重構(gòu)后的最終代碼

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);

上面的代碼里應(yīng)用了一些技術(shù)點,我們來逐一看一下:

  1. 首先,questionCreator 方法的創(chuàng)建,可以讓我們使用模板方法模式將處理問題的功能 delegat 給針對每個問題類型的擴展代碼 renderInput 上。
  2. 其次,我們用一個私有的 spec 屬性替換掉了前面 question 方法的構(gòu)造函數(shù)屬性,因為我們封裝了 render 行為進(jìn)行操作,不再需要把這些屬性暴露給外部代碼了。
  3. 第三,我們?yōu)槊總€問題類型創(chuàng)建一個對象進(jìn)行各自的代碼實現(xiàn),但每個實現(xiàn)里都必須包含 renderInput 方法以便覆蓋 questionCreator 方法里的 renderInput 代碼,這就是我們常說的策略模式。
  4. 通過重構(gòu),我們可以去除不必要的問題類型的枚舉 AnswerType,而且可以讓 choices 作為 choiceQuestionCreator 函數(shù)的必選參數(shù)(之前的版本是一個可選參數(shù))。

總結(jié)

重構(gòu)以后的版本的 view 對象可以很清晰地進(jìn)行新的擴展了,為不同的問題類型擴展新的對象,然后聲明 questions 集合的時候再里面指定類型就行了,view 對象本身不再修改任何改變,從而達(dá)到了開閉原則的要求。

另:懂 C#的話,不知道看了上面的代碼后是否和多態(tài)的實現(xiàn)有些類似?其實上述的代碼用原型也是可以實現(xiàn)的,大家可以自行研究一下。