本章我們要講解的是 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)一下這個代碼,以便在出現(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ù)點,我們來逐一看一下:
重構(gòu)以后的版本的 view 對象可以很清晰地進(jìn)行新的擴展了,為不同的問題類型擴展新的對象,然后聲明 questions 集合的時候再里面指定類型就行了,view 對象本身不再修改任何改變,從而達(dá)到了開閉原則的要求。
另:懂 C#的話,不知道看了上面的代碼后是否和多態(tài)的實現(xiàn)有些類似?其實上述的代碼用原型也是可以實現(xiàn)的,大家可以自行研究一下。