深入理解JavaScript系列(7) S.O.L.I.D五大原则之开闭原则OCP


Posted in Javascript onJanuary 15, 2012

前言
本章我们要讲解的是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);

重构后的最终代码

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);

上面的代码里应用了一些技术点,我们来逐一看一下:
首先,questionCreator方法的创建,可以让我们使用模板方法模式将处理问题的功能delegat给针对每个问题类型的扩展代码renderInput上。
其次,我们用一个私有的spec属性替换掉了前面question方法的构造函数属性,因为我们封装了render行为进行操作,不再需要把这些属性暴露给外部代码了。
第三,我们为每个问题类型创建一个对象进行各自的代码实现,但每个实现里都必须包含renderInput方法以便覆盖questionCreator方法里的renderInput代码,这就是我们常说的策略模式。
通过重构,我们可以去除不必要的问题类型的枚举AnswerType,而且可以让choices作为choiceQuestionCreator函数的必选参数(之前的版本是一个可选参数)。
总结
重构以后的版本的view对象可以很清晰地进行新的扩展了,为不同的问题类型扩展新的对象,然后声明questions集合的时候再里面指定类型就行了,view对象本身不再修改任何改变,从而达到了开闭原则的要求。
另:懂C#的话,不知道看了上面的代码后是否和多态的实现有些类似?其实上述的代码用原型也是可以实现的,大家可以自行研究一下。
Javascript 相关文章推荐
利用jQuery 实现GridView异步排序、分页的代码
Feb 06 Javascript
谈一谈js中的执行环境及作用域
Mar 30 Javascript
轻松实现js选项卡切换效果
Sep 24 Javascript
Angularjs 设置全局变量的方法总结
Oct 20 Javascript
jQuery Validate设置onkeyup验证的实例代码
Dec 09 Javascript
jquery获取transform里的值实现方法
Dec 12 jQuery
Vue 2.5.2下axios + express 本地请求404的解决方法
Feb 21 Javascript
JS实现调用本地摄像头功能示例
May 18 Javascript
Taro集成Redux快速上手的方法示例
Jun 21 Javascript
JS实现鼠标拖拽盒子移动及右键点击盒子消失效果示例
Jan 29 Javascript
详解微信小程序实现跑马灯效果(附完整代码)
Apr 29 Javascript
JS中实现浅拷贝和深拷贝的代码详解
Jun 05 Javascript
深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP
Jan 15 #Javascript
深入理解JavaScript系列(6) 强大的原型和原型链
Jan 15 #Javascript
深入理解JavaScript系列(4) 立即调用的函数表达式
Jan 15 #Javascript
深入理解JavaScript系列(3) 全面解析Module模式
Jan 15 #Javascript
深入理解JavaScript系列(2) 揭秘命名函数表达式
Jan 15 #Javascript
深入理解JavaScript系列(1) 编写高质量JavaScript代码的基本要点
Jan 15 #Javascript
Prototype源码浅析 String部分(三)之HTML字符串处理
Jan 15 #Javascript
You might like
超神学院:鹤熙已踏入神圣领域,实力不比凯莎弱
2020/03/02 国漫
WordPress自定义时间显示格式
2015/03/27 PHP
[原创]js获取数组任意个不重复的随机数组元素
2010/03/15 Javascript
Extjs中DisplayField的日期或者数字格式化扩展
2010/09/03 Javascript
用JS实现一个TreeMenu效果分享
2011/08/28 Javascript
js实现拖拽 闭包函数详细介绍
2012/11/25 Javascript
js获取电脑分辨率的思路及操作
2013/11/22 Javascript
JS完整获取IE浏览器信息包括类型、版本、语言等等
2014/05/22 Javascript
js判断浏览器是否支持html5
2014/08/17 Javascript
javascript实现动态模态绑定grid过程代码
2014/09/22 Javascript
AngularJS实现表单验证
2015/01/28 Javascript
用NodeJS实现批量查询地理位置的经纬度接口
2016/08/16 NodeJs
angularjs实现猜大小功能
2017/10/23 Javascript
Webpack devServer中的 proxy 实现跨域的解决
2018/06/15 Javascript
Node.js命令行/批处理中如何更改Linux用户密码浅析
2018/07/22 Javascript
小程序显示弹窗时禁止下层的内容滚动实现方法
2019/03/20 Javascript
react写一个select组件的实现代码
2019/04/03 Javascript
简单了解vue.js数组的常用操作
2019/06/17 Javascript
a标签调用js的方法总结
2019/09/05 Javascript
python连接mysql数据库示例(做增删改操作)
2013/12/31 Python
在Django的URLconf中使用命名组的方法
2015/07/18 Python
Python实现的RSS阅读器实例
2015/07/25 Python
详解Python list 与 NumPy.ndarry 切片之间的对比
2017/07/24 Python
Python登录系统界面实现详解
2019/06/25 Python
Python 保持登录状态进行接口测试的方法示例
2019/08/06 Python
通过Turtle库在Python中绘制一个鼠年福鼠
2020/02/03 Python
使用python采集Excel表中某一格数据
2020/05/14 Python
Python 列表反转显示的四种方法
2020/11/16 Python
高中军训感言500字
2014/02/24 职场文书
《池塘边的叫声》教学反思
2014/04/12 职场文书
学校党的群众路线教育实践活动对照检查材料
2014/09/24 职场文书
2014年班主任工作总结
2014/11/08 职场文书
2015年党员自评材料
2014/12/17 职场文书
企业计划生育责任书
2015/05/09 职场文书
读鲁迅先生的经典名言
2019/08/20 职场文书
win10电脑右下角输入法图标不见了?Win10右下角不显示输入法的解决方法
2022/07/23 数码科技