深入理解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 相关文章推荐
javascript 嵌套的函数(作用域链)
Mar 15 Javascript
Firefox下提示illegal character并出现乱码的原因
Mar 25 Javascript
说明你的Javascript技术很烂的五个原因
Apr 26 Javascript
javascript-简单的日历实现及Date对象语法介绍(附图)
May 30 Javascript
jQuery.lazyload+masonry改良图片瀑布流代码
Jun 20 Javascript
使用AngularJS处理单选框和复选框的简单方法
Jun 19 Javascript
jQuery Validate表单验证深入学习
Dec 18 Javascript
学习使用bootstrap的modal和carousel
Dec 09 Javascript
纯js实现倒计时功能
Jan 06 Javascript
easyui datagrid 表格中操作栏 按钮图标不显示的解决方法
Jul 27 Javascript
详解Vue 动态组件与全局事件绑定总结
Nov 11 Javascript
微信小程序之滑动页面隐藏和显示组件功能的实现代码
Jun 19 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
如何对PHP程序中的常见漏洞进行攻击
2006/10/09 PHP
PHP中把stdClass Object转array的几个方法
2014/05/08 PHP
PHP实现定时执行任务的方法
2014/10/05 PHP
ThinkPHP控制器间实现相互调用的方法
2014/10/31 PHP
PHP提高编程效率的20个要点
2015/09/23 PHP
php代码架构的八点注意事项
2016/01/25 PHP
PHP写的简单数字验证码实例
2017/05/23 PHP
PHP删除数组中指定下标的元素方法
2018/02/03 PHP
location.search在客户端获取Url参数的方法
2010/06/08 Javascript
jQuery.parseJSON(json)将JSON字符串转换成js对象
2014/07/27 Javascript
node.js中的buffer.toJSON方法使用说明
2014/12/14 Javascript
JavaScript中Number.MIN_VALUE属性的使用示例
2015/06/04 Javascript
jQuery实现tab选项卡效果的方法
2015/07/08 Javascript
JS实现鼠标框选效果完整实例
2016/06/20 Javascript
JavaScript中Form表单技术汇总(推荐)
2016/06/26 Javascript
javascript创建含数字字母的随机字符串方法总结
2016/08/01 Javascript
JavaScript数组去重由慢到快由繁到简(优化篇)
2016/08/26 Javascript
js生成随机颜色方法代码分享(三种)
2016/12/29 Javascript
Javascript中的神器——Promise
2017/02/08 Javascript
python实现简单的计时器功能函数
2015/03/14 Python
Python3连接MySQL(pymysql)模拟转账实现代码
2016/05/24 Python
Python编程中装饰器的使用示例解析
2016/06/20 Python
elasticsearch python 查询的两种方法
2019/08/04 Python
Python3 翻转二叉树的实现
2019/09/30 Python
python 定义类时,实现内部方法的互相调用
2019/12/25 Python
python的sys.path模块路径添加方式
2020/03/09 Python
Python类super()及私有属性原理解析
2020/06/15 Python
python使用多线程查询数据库的实现示例
2020/08/17 Python
一款基于css3的动画按钮代码教程
2014/11/23 HTML / CSS
设计毕业生简历中的自我评价
2013/10/01 职场文书
庆七一活动方案
2014/01/25 职场文书
新闻编辑专业毕业自荐书范文
2014/02/05 职场文书
岗位明星事迹材料
2014/05/18 职场文书
2015年人事工作总结范文
2015/04/09 职场文书
汽车修理厂管理制度
2015/08/05 职场文书
Python爬虫入门案例之爬取二手房源数据
2021/10/16 Python