前言
本章我們要講解的是S.O.L.I.D五大原則JavaScript語(yǔ)言實(shí)現(xiàn)的第2篇,,開(kāi)閉原則OCP(The Open/Closed Principle ),。
開(kāi)閉原則的描述是:
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. 軟件實(shí)體(類,模塊,,方法等等)應(yīng)當(dāng)對(duì)擴(kuò)展開(kāi)放,,對(duì)修改關(guān)閉,,即軟件實(shí)體應(yīng)當(dāng)在不修改的前提下擴(kuò)展,。
open for extension(對(duì)擴(kuò)展開(kāi)放)的意思是說(shuō)當(dāng)新需求出現(xiàn)的時(shí)候,可以通過(guò)擴(kuò)展現(xiàn)有模型達(dá)到目的,。而Close for modification(對(duì)修改關(guān)閉)的意思是說(shuō)不允許對(duì)該實(shí)體做任何修改,,說(shuō)白了,就是這些需要執(zhí)行多樣行為的實(shí)體應(yīng)該設(shè)計(jì)成不需要修改就可以實(shí)現(xiàn)各種的變化,,堅(jiān)持開(kāi)閉原則有利于用最少的代碼進(jìn)行項(xiàng)目維護(hù),。
英文原文:http:///derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/
問(wèn)題代碼
為了直觀地描述,我們來(lái)舉個(gè)例子演示一下,,下屬代碼是動(dòng)態(tài)展示question列表的代碼(沒(méi)有使用開(kāi)閉原則),。
// 問(wèn)題類型 var AnswerType = { Choice: 0, Input: 1 };
// 問(wèn)題實(shí)體 function question(label, answerType, choices) { return { label: label, answerType: answerType, choices: choices // 這里的choices是可選參數(shù) }; }
var view = (function () { // render一個(gè)問(wèn)題 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 { // 遍歷所有的問(wèn)題列表進(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對(duì)象里包含一個(gè)render方法用來(lái)展示question列表,,展示的時(shí)候根據(jù)不同的question類型使用不同的展示方式,,一個(gè)question包含一個(gè)label和一個(gè)問(wèn)題類型以及choices的選項(xiàng)(如果是選擇類型的話),。如果問(wèn)題類型是Choice那就根據(jù)選項(xiàng)生產(chǎn)一個(gè)下拉菜單,如果類型是Input,,那就簡(jiǎn)單地展示input輸入框,。
該代碼有一個(gè)限制,就是如果再增加一個(gè)question類型的話,,那就需要再次修改renderQuestion里的條件語(yǔ)句,,這明顯違反了開(kāi)閉原則。
重構(gòu)代碼
讓我們來(lái)重構(gòu)一下這個(gè)代碼,,以便在出現(xiàn)新question類型的情況下允許擴(kuò)展view對(duì)象的render能力,而不需要修改view對(duì)象內(nèi)部的代碼,。
先來(lá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沒(méi)有實(shí)現(xiàn),,主要目的是讓各自問(wè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)椴煌膯?wèn)題類型有不同的實(shí)現(xiàn)
questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); return questionWrapper; };
return that; }
該代碼的作用組合要是render一個(gè)問(wèn)題,同時(shí)提供一個(gè)未實(shí)現(xiàn)的renderInput方法以便其他function可以覆蓋,,以使用不同的問(wèn)題類型,,我們繼續(xù)看一下每個(gè)問(wèn)題類型的實(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),通過(guò)內(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()); } } };
所以我們聲明問(wèn)題的時(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?' }) ];
最終的使用代碼,,我們可以這樣來(lái)用:
var questionRegion = document.getElementById('questions');
view.render(questionRegion, questions);
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ù)點(diǎn),,我們來(lái)逐一看一下:
- 首先,questionCreator方法的創(chuàng)建,,可以讓我們使用模板方法模式將處理問(wèn)題的功能delegat給針對(duì)每個(gè)問(wèn)題類型的擴(kuò)展代碼renderInput上,。
- 其次,我們用一個(gè)私有的spec屬性替換掉了前面question方法的構(gòu)造函數(shù)屬性,,因?yàn)槲覀兎庋b了render行為進(jìn)行操作,,不再需要把這些屬性暴露給外部代碼了。
- 第三,,我們?yōu)槊總€(gè)問(wèn)題類型創(chuàng)建一個(gè)對(duì)象進(jìn)行各自的代碼實(shí)現(xiàn),,但每個(gè)實(shí)現(xiàn)里都必須包含renderInput方法以便覆蓋questionCreator方法里的renderInput代碼,這就是我們常說(shuō)的策略模式,。
通過(guò)重構(gòu),,我們可以去除不必要的問(wèn)題類型的枚舉AnswerType,而且可以讓choices作為choiceQuestionCreator函數(shù)的必選參數(shù)(之前的版本是一個(gè)可選參數(shù)),。
總結(jié)
重構(gòu)以后的版本的view對(duì)象可以很清晰地進(jìn)行新的擴(kuò)展了,,為不同的問(wèn)題類型擴(kuò)展新的對(duì)象,然后聲明questions集合的時(shí)候再里面指定類型就行了,,view對(duì)象本身不再修改任何改變,,從而達(dá)到了開(kāi)閉原則的要求。
另:懂C#的話,不知道看了上面的代碼后是否和多態(tài)的實(shí)現(xiàn)有些類似,?其實(shí)上述的代碼用原型也是可以實(shí)現(xiàn)的,,大家可以自行研究一下。
同步與推薦
本文已同步至目錄索引:深入理解JavaScript系列
深入理解JavaScript系列文章,,包括了原創(chuàng),,翻譯,轉(zhuǎn)載等各類型的文章,,如果對(duì)你有用,,請(qǐng)推薦支持一把,給大叔寫作的動(dòng)力,。
|