開閉原則的核心是:對(duì)擴(kuò)展開放峦树,對(duì)修改關(guān)閉
白話意思就是我們改變一個(gè)軟件時(shí)(比如擴(kuò)展其他功能)额划,應(yīng)該通過擴(kuò)展的方式來達(dá)到軟件的改變工闺,而不應(yīng)愛修改原有代碼來實(shí)現(xiàn)變化
說白了度苔,就是這些需要執(zhí)行多樣行為的實(shí)體應(yīng)該設(shè)計(jì)成不需要修改就可以實(shí)現(xiàn)各種的變化,堅(jiān)持開閉原則有利于用最少的代碼進(jìn)行項(xiàng)目維護(hù)暂吉。
下面是使用開閉原則的一個(gè)簡(jiǎn)單示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="test"></div>
</body>
<script type="text/javascript">
//一個(gè)面向?qū)ο蟮腏S例子胖秒,很好的支持了開閉原則
function HtmlControl(options) { //定義一個(gè)方法
var el = options.element; //el賦值為dom元素
//下面是為el元素添加參數(shù)樣式
el.style.width = options.width;
el.style.height = options.height;
el.style.top = options.top;
el.style.background = options.background;
el.innerHTML=options.text;
console.log(el)
}
var option = { //為方法定義一個(gè)參數(shù)對(duì)象
element: document.getElementById('test'),
left: 50,
top: 0,
width: 100,
height: 200,
background: '#ccc',
text:'什么是開閉原則'
}
option.background = 'red'; //對(duì)參數(shù)對(duì)象進(jìn)行擴(kuò)展
HtmlControl(option); //調(diào)用
</script>
</html>
這是一個(gè)簡(jiǎn)單的例子,意思就是說封裝一個(gè)功能慕的,這個(gè)功能有默認(rèn)參數(shù)阎肝,對(duì)外提供接口,接口參數(shù)可擴(kuò)展改變业稼,但是需要修改功能是不可以的盗痒,也就是說在功能接口內(nèi)擴(kuò)展修改該功能,就可以得到不同的效果低散。
為了直觀地描述,我們來舉個(gè)例子演示一下骡楼,下屬代碼是動(dòng)態(tài)展示question列表的代碼(沒有使用開閉原則)熔号。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="questions"></div>
</body>
<script type="text/javascript">
// 問題類型
var AnswerType = {
Choice: 0,
Input: 1
};
// 問題實(shí)體
function question(label, answerType, choices) {
return {
label: label,
answerType: answerType,
choices: choices // 這里的choices是可選參數(shù)
};
}
var view = (function() {
// render一個(gè)問題
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);//創(chuàng)建文本節(jié)點(diǎn)
questionLabel.appendChild(label);//在指定節(jié)點(diǎn)的最后一個(gè)子節(jié)點(diǎn)列表之后添加一個(gè)新的子節(jié)點(diǎn)。
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);
</script>
</html>
實(shí)現(xiàn)效果
上面的代碼鸟整,view對(duì)象里包含一個(gè)render方法用來展示question列表引镊,展示的時(shí)候根據(jù)不同的question類型使用不同的展示方式,一個(gè)question包含一個(gè)label和一個(gè)問題類型以及choices的選項(xiàng)(如果是選擇類型的話)篮条。如果問題類型是Choice那就根據(jù)選項(xiàng)生產(chǎn)一個(gè)下拉菜單弟头,如果類型是Input,那就簡(jiǎn)單地展示input輸入框涉茧。
該代碼有一個(gè)限制赴恨,就是如果再增加一個(gè)question類型的話,那就需要再次修改renderQuestion里的條件語句伴栓,這明顯違反了開閉原則伦连。
重構(gòu)代碼
讓我們來重構(gòu)一下這個(gè)代碼雨饺,以便在出現(xiàn)新question類型的情況下允許擴(kuò)展view對(duì)象的render能力,而不需要修改view對(duì)象內(nèi)部的代碼惑淳。
先來創(chuàng)建一個(gè)通用的questionCreator函數(shù):
function questionCreator(spec, my) {
var that = {};
my = my || {};
my.label = spec.label;
my.renderInput = function () {
throw not implemented;
// 這里renderInput沒有實(shí)現(xiàn)额港,主要目的是讓各自問題類型的實(shí)現(xiàn)代碼去覆蓋整個(gè)方法
};
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()
// 因?yàn)椴煌膯栴}類型有不同的實(shí)現(xiàn)
questionWrapper.appendChild(questionLabel);
questionWrapper.appendChild(answer);
return questionWrapper;
};
return that;
}
該代碼的作用組合要是render一個(gè)問題,同時(shí)提供一個(gè)未實(shí)現(xiàn)的renderInput方法以便其他function可以覆蓋歧焦,以使用不同的問題類型移斩,我們繼續(xù)看一下每個(gè)問題類型的實(shí)現(xiàn)代碼:
function choiceQuestionCreator(spec) {
var my = {},
that = questionCreator(spec, my);
// choice類型的renderInput實(shí)現(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實(shí)現(xiàn)
my.renderInput = function () {
var input = document.createElement('input');
input.type = 'text';
return input;
};
return that;
}
choiceQuestionCreator函數(shù)和inputQuestionCreator函數(shù)分別對(duì)應(yīng)下拉菜單和input輸入框的renderInput實(shí)現(xiàn),通過內(nèi)部調(diào)用統(tǒng)一的questionCreator(spec, my)然后返回that對(duì)象(同一類型哦)绢馍。
view對(duì)象的代碼就很固定了向瓷。
var view = {
render: function(target, questions) {
for (var i = 0; i < questions.length; i++) {
target.appendChild(questions[i].render());
}
}
};
所以我們聲明問題的時(shí)候只需要這樣做,就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);
完整代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS開閉原則</title>
</head>
<body>
<div id="questions"></div>
</body>
<script type="text/javascript">
function questionCreator(spec, my) {
var that = {};
my = my || {};
my.label = spec.label;
my.renderInput = function() {
// 1這里renderInput沒有實(shí)現(xiàn)风罩,主要目的是讓各自問題類型的實(shí)現(xiàn)代碼去覆蓋整個(gè)方法
};
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();
console.log(answer)
// 該render方法是同樣的粗合理代碼
// 唯一的不同就是上面的一句my.renderInput()
// 因?yàn)椴煌膯栴}類型有不同的實(shí)現(xiàn)
questionWrapper.appendChild(questionLabel);
questionWrapper.appendChild(answer);
return questionWrapper;
};
return that;
}
function choiceQuestionCreator(spec) {
var my = {};
var that = questionCreator(spec, my);
// choice類型的renderInput實(shí)現(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;
};
//返回一個(gè)方法
return that;
}
function inputQuestionCreator(spec) {
var my = {},
that = questionCreator(spec, my);
// input類型的renderInput實(shí)現(xiàn)
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({//運(yùn)行choiceQuestionCreator函數(shù)
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);
</script>
</html>
重構(gòu)后的最終代碼
上面的代碼里應(yīng)用了一些技術(shù)點(diǎn),我們來逐一看一下:
首先舵稠,questionCreator方法的創(chuàng)建超升,可以讓我們使用模板方法模式將處理問題的功能delegat給針對(duì)每個(gè)問題類型的擴(kuò)展代碼renderInput上。
其次哺徊,我們用一個(gè)私有的spec屬性替換掉了前面question方法的構(gòu)造函數(shù)屬性室琢,因?yàn)槲覀兎庋b了render行為進(jìn)行操作,不再需要把這些屬性暴露給外部代碼了落追。
第三盈滴,我們?yōu)槊總€(gè)問題類型創(chuàng)建一個(gè)對(duì)象進(jìn)行各自的代碼實(shí)現(xiàn),但每個(gè)實(shí)現(xiàn)里都必須包含renderInput方法以便覆蓋questionCreator方法里的renderInput代碼轿钠,這就是我們常說的策略模式巢钓。
通過重構(gòu),我們可以去除不必要的問題類型的枚舉AnswerType疗垛,而且可以讓choices作為choiceQuestionCreator函數(shù)的必選參數(shù)(之前的版本是一個(gè)可選參數(shù))症汹。
總結(jié)
重構(gòu)以后的版本的view對(duì)象可以很清晰地進(jìn)行新的擴(kuò)展了,為不同的問題類型擴(kuò)展新的對(duì)象贷腕,然后聲明questions集合的時(shí)候再里面指定類型就行了背镇,view對(duì)象本身不再修改任何改變,從而達(dá)到了開閉原則的要求泽裳。