深入理解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 EasyUI API 中文文档 - PropertyGrid属性表格
Nov 18 Javascript
javascript实现给定半径求出圆的面积
Jun 26 Javascript
JavaScript中几种排序算法的简单实现
Jul 29 Javascript
JavaScript各类型的关系图解
Oct 16 Javascript
基于Javascript实现返回顶部按钮
Feb 29 Javascript
AngularJS优雅的自定义指令
Jul 01 Javascript
js中获取键盘事件的简单实现方法
Oct 10 Javascript
Bootstrap源码解读标签、徽章、缩略图和警示框(8)
Dec 26 Javascript
BootStrap与Select2使用小结
Feb 17 Javascript
mint-ui在vue中的使用示例
Apr 05 Javascript
解决layui-table单元格设置为百分比在ie8下不能自适应的问题
Sep 28 Javascript
vant中的toast层级改变操作
Nov 04 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
yii,CI,yaf框架+smarty模板使用方法
2015/12/29 PHP
PHP实现模拟http请求的方法分析
2017/12/20 PHP
WEB 浏览器兼容 推荐收藏
2010/05/14 Javascript
浏览器脚本兼容 文本框中,回车键触发事件的兼容
2010/06/21 Javascript
使用Grunt.js管理你项目的应用说明
2013/04/24 Javascript
基于Unit PNG Fix.js有时候在ie6下不正常的解决办法
2013/06/26 Javascript
JS 毫秒转时间示例代码
2013/09/22 Javascript
JavaScript判断FileUpload控件上传文件类型
2015/09/28 Javascript
浅谈FastClick 填坑及源码解析
2018/03/02 Javascript
JS实现左边列表移到到右边列表功能
2018/03/28 Javascript
Node.js中,在cmd界面,进入退出Node.js运行环境的方法
2018/05/12 Javascript
Vue无限滑动周选择日期的组件的示例代码
2018/07/18 Javascript
微信小程序引入VANT组件的方法步骤
2019/09/19 Javascript
React 实现车牌键盘的示例代码
2019/12/20 Javascript
JS操作Fckeditor的一些常用方法(获取、插入等)
2020/02/19 Javascript
[03:08]TI9战队档案 - Vici Gaming
2019/08/20 DOTA
Python实现从脚本里运行scrapy的方法
2015/04/07 Python
Python编程实现蚁群算法详解
2017/11/13 Python
详解appium+python 启动一个app步骤
2017/12/20 Python
python3安装pip3(install pip3 for python 3.x)
2018/04/03 Python
pygame游戏之旅 添加游戏暂停功能
2018/11/21 Python
python+pyqt5实现KFC点餐收银系统
2019/01/24 Python
PyTorch基本数据类型(一)
2019/05/22 Python
Python matplotlib实时画图案例
2020/04/23 Python
通过自学python能找到工作吗
2020/06/21 Python
如何验证python安装成功
2020/07/06 Python
CSS3五个技巧给你的网站带来出色的效果
2009/04/02 HTML / CSS
HTML5和以前HTML4的区别整理
2013/10/20 HTML / CSS
中国宠物用品商城:E宠商城
2016/08/27 全球购物
英国电子产品购物网站:Tech in the basket
2019/11/08 全球购物
北京捷通华声语音技术有限公司Java软件工程师笔试题
2012/04/10 面试题
化学相关工作求职信
2013/10/02 职场文书
社区平安建设汇报材料
2014/08/14 职场文书
生日赠语
2015/06/23 职场文书
python基于tkinter制作无损音乐下载工具
2021/03/29 Python
使用pycharm运行flask应用程序的详细教程
2021/06/07 Python