深入理解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 相关文章推荐
js图片向右一张张滚动效果实例代码
Nov 23 Javascript
javascript算法题:求任意一个1-9位不重复的N位数在该组合中的大小排列序号
Apr 01 Javascript
jQuery插件ImageDrawer.js实现动态绘制图片动画(附源码下载)
Feb 25 Javascript
Javascript中Date类型和Math类型详解
Feb 27 Javascript
gulp教程_从入门到项目中快速上手使用方法
Sep 14 Javascript
vue-cli如何引入bootstrap工具的方法
Oct 19 Javascript
vue中子组件调用兄弟组件方法
Jul 06 Javascript
vue+iview 实现可编辑表格的示例代码
Oct 31 Javascript
node.js中module模块的功能理解与用法实例分析
Feb 14 Javascript
openlayers 3实现车辆轨迹回放
Sep 24 Javascript
解决elementui表格操作列自适应列宽
Dec 28 Javascript
vue3种table表格选项个数的控制方法
Apr 14 Vue.js
深入理解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
php中通过smtp发邮件的类,测试通过
2007/01/22 PHP
php实现的遍历文件夹下所有文件,编辑删除
2010/01/05 PHP
在Win7 中为php扩展配置Xcache
2014/10/08 PHP
ThinkPHP框架分布式数据库连接方法详解
2017/03/14 PHP
PHP实现微信模拟登陆并给用户发送消息的方法【文字,图片,图文】
2017/06/29 PHP
免费空间广告万能消除代码
2006/09/04 Javascript
参考:关于Javascript中实现暂停的几篇文章
2007/03/04 Javascript
javascript实现动态增加删除表格行(兼容IE/FF)
2007/04/02 Javascript
javascript英文日期(有时间)选择器
2007/05/02 Javascript
jQuery最佳实践完整篇
2011/08/20 Javascript
jquery.pagination.js 无刷新分页实现步骤分享
2012/05/23 Javascript
仿新浪微博返回顶部的jquery实现代码
2012/10/01 Javascript
js中复制行和删除行的操作实例
2013/06/25 Javascript
实现图片预加载的三大方法及优缺点分析
2014/11/19 Javascript
JavaScript中实现继承的三种方式和实例
2015/01/29 Javascript
jQuery中animate动画第二次点击事件没反应
2015/05/07 Javascript
基于dropdown.js实现的两款美观大气的二级导航菜单
2015/09/02 Javascript
利用jsonp跨域调用百度js实现搜索框智能提示
2016/08/24 Javascript
Webpack如何引入bootstrap的方法
2017/06/17 Javascript
JavaScript-定时器0~9抽奖系统详解(代码)
2017/08/16 Javascript
AngularJS路由删除#符号解决的办法
2017/09/28 Javascript
vue中v-model的应用及使用详解
2018/06/27 Javascript
jQuery使用bind动态绑定事件无效的处理方法
2018/12/11 jQuery
echarts实现折线图的拖拽效果
2019/12/19 Javascript
小程序如何定位所在城市及发起周边搜索
2020/02/11 Javascript
python使用PyGame播放Midi和Mp3文件的方法
2015/04/24 Python
Python求算数平方根和约数的方法汇总
2016/03/09 Python
Python开发的十个小贴士和技巧及长常犯错误
2018/09/27 Python
对python内置map和six.moves.map的区别详解
2018/12/19 Python
PyQt中使用QtSql连接MySql数据库的方法
2020/07/28 Python
完美解决torch.cuda.is_available()一直返回False的玄学方法
2021/02/06 Python
试用期员工考核制度
2014/01/22 职场文书
党员违纪检讨书
2014/02/18 职场文书
大学生党员自我批评思想汇报
2014/10/10 职场文书
公司开业主持词
2015/07/02 职场文书
解决Golang中ResponseWriter的一个坑
2021/04/27 Golang