前言
本章我们要解说的是S.O.L.I.D五大原则JavaScript语言实现的第2篇,开闭原则OCP(The Open/Closed Principle )。
开闭原则的描写叙述是:
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. 软件实体(类。模块,方法等等)应当对扩展开放。对改动关闭,即软件实体应当在不改动的前提下扩展。
open for extension(对扩展开放)的意思是说当新需求出现的时候,能够通过扩展现有模型达到目的。而Close for modification(对改动关闭)的意思是说不同意对该实体做不论什么改动,说白了,就是这些须要运行多样行为的实体应该设计成不须要改动就能够实现各种的变化。坚持开闭原则有利于用最少的代码进行项目维护。
英文原文:http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/
问题代码
为了直观地描写叙述。我们来举个样例演示一下,下属代码是动态展示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对象内部的代码。
先来创建一个通用的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对象本身不再改动不论什么改变。从而达到了开闭原则的要求。
另:懂C#的话,不知道看了上面的代码后是否和多态的实现有些类似?事实上上述的代码用原型也是能够实现的。大家能够自行研究一下。