以前簡單介紹了常見設計模式遵循的設計原則--單一職責原則,這篇介紹一下另一個至關重要和具備指導性的一個原則,開放關閉原則。可是,關於這一個原則的使用,經驗是至關重要的一個因素。設計模式
可是我的感受開閉原則多是設計模式幾大原則中定義最模糊的一個了,它只告訴咱們對擴展開放,對修改關閉,但是到底如何才能作到對擴展開放,對修改關閉,並無明確的告訴咱們。之前,若是有人說「你進行設計的時候必定要遵照開閉原則」,會讓人以爲什麼都沒說,但貌似又什麼都說了。由於開閉原則真的太虛了。架構
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
定義:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。app
問題由來:在軟件的生命週期內,由於變化、升級和維護等緣由須要對軟件原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使咱們不得不對整個功能進行重構,而且須要原有代碼通過從新測試。框架
解決方案:當軟件須要變化時,儘可能經過擴展軟件實體的行爲來實現變化,而不是經過修改已有的代碼來實現變化。函數
藉助下面這個例子,深刻學習和理解所謂的開閉原則的思想。代碼是動態展現question列表的代碼(沒有使用開閉原則)。學習
// 問題類型 var AnswerType = { Choice: 0, Input: 1 }; // 問題實體 function question(label, answerType, choices) { return { label: label, answerType: answerType, choices: choices // 這裏的choices是可選參數 }; } 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'; // 根據不一樣的類型展現不一樣的代碼:分別是下拉菜單和輸入框兩種 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 { // 遍歷全部的問題列表進行展現 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列表,展現的時候根據不一樣的question類型使用不一樣的展現方式,一個question包含一個label和一個問題類型以及choices的選項(若是是選擇類型的話)。若是問題類型是Choice那就根據選項生產一個下拉菜單,若是類型是Input,那就簡單地展現input輸入框。測試
該代碼有一個限制,就是若是再增長一個question類型的話,那就須要再次修改renderQuestion裏的條件語句,這明顯違反了開閉原則。設計
讓咱們來重構一下這個代碼,以便在出現新question類型的狀況下容許擴展view對象的render能力,而不須要修改view對象內部的代碼。code
先來建立一個通用的questionCreator函數:對象
function questionCreator(spec, my) { var that = {}; my = my || {}; my.label = spec.label; my.renderInput = function () { throw "not implemented"; // 這裏renderInput沒有實現,主要目的是讓各自問題類型的實現代碼去覆蓋整個方法 }; 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() // 由於不一樣的問題類型有不一樣的實現 questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); return questionWrapper; }; return that; }
該代碼的做用組合要是render一個問題,同時提供一個未實現的renderInput方法以便其餘function能夠覆蓋,以使用不一樣的問題類型,咱們繼續看一下每一個問題類型的實現代碼:
function choiceQuestionCreator(spec) { var my = {}, that = questionCreator(spec, my); // choice類型的renderInput實現 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實現 my.renderInput = function () { var input = document.createElement('input'); input.type = 'text'; return input; }; return that; }
choiceQuestionCreator函數和inputQuestionCreator函數分別對應下拉菜單和input輸入框的renderInput實現,經過內部調用統一的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);
上面的代碼裏應用了一些技術點,這裏總結一下:
首先,questionCreator方法的建立,可讓咱們使用模板方法模式將處理問題的功能delegat給針對每一個問題類型的擴展代碼renderInput上。
其次,咱們用一個私有的spec屬性替換掉了前面question方法的構造函數屬性,由於咱們封裝了render行爲進行操做,再也不須要把這些屬性暴露給外部代碼了。
第三,咱們爲每一個問題類型建立一個對象進行各自的代碼實現,但每一個實現裏都必須包含renderInput方法以便覆蓋questionCreator方法裏的renderInput代碼,這就是咱們常說的策略模式。經過完善以後,咱們能夠去除沒必要要的問題類型的枚舉AnswerType,並且可讓choices做爲choiceQuestionCreator函數的必選參數(以前的版本是一個可選參數)。
重構之後的版本的view對象能夠很清晰地進行新的擴展了,爲不一樣的問題類型擴展新的對象,而後聲明questions集合的時候再裏面指定類型就好了,view對象自己再也不修改任何改變,從而達到了開閉原則的要求。
搞論文準備答辯的時候,仔細思考以及仔細閱讀不少設計模式的文章後,終於對開閉原則有了一點認識。其實,咱們遵循設計模式其餘幾大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要咱們其餘原則遵照的好了,設計出的軟件天然是符合開閉原則的,這個開閉原則更像是這些原則遵照程度的「平均得分」,這些原則遵照的好,平均分天然就高,說明軟件設計開閉原則遵照的好;這些原則遵照的很差,則說明開閉原則遵照的很差。
開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。由於抽象靈活性好,適應性廣,只要抽象的合理,能夠基本保持軟件架構的穩定。而軟件中易變的細節,咱們用從抽象派生的實現類來進行擴展,當軟件須要發生變化時,咱們只須要根據需求從新派生一個實現類來擴展就能夠了。固然前提是咱們的抽象要合理,要對需求的變動有前瞻性和預見性才行。