深入理解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 模式实例 观察者模式
Oct 24 Javascript
jquery+ashx无刷新GridView数据显示插件(实现分页、排序、过滤功能)
Apr 25 Javascript
jquery的flexigrid无法显示数据提示获取到数据
Jul 19 Javascript
打造个性化的功能强大的Jquery虚拟键盘(VirtualKeyboard)
Oct 11 Javascript
初步使用bootstrap快速创建页面
Mar 03 Javascript
JS实时弹出新消息提示框并有提示音响起的实现代码
Apr 20 Javascript
vue.js单页面应用实例的简单实现
Apr 10 Javascript
JavaScript fetch接口案例解析
Aug 30 Javascript
使用Bootstrap做一个朝代历史表
Dec 10 Javascript
Js Snowflake(雪花算法)生成随机ID的实现方法
Aug 26 Javascript
JavaScript编码小技巧分享
Sep 17 Javascript
微信小程序实现打卡签到页面
Sep 21 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
基于mysql的论坛(7)
2006/10/09 PHP
PHP 木马攻击防御技巧
2009/06/13 PHP
PHP批量查询WordPress留言者E-mail地址实现方法
2015/02/15 PHP
php简单统计字符串单词数量的方法
2015/06/19 PHP
WordPress中自定义后台管理界面配色方案的小技巧
2015/12/29 PHP
PHP的简单跳转提示的实现详解
2019/03/14 PHP
Javascript表格翻页效果的具体实现
2013/10/05 Javascript
jquery链式操作的正确使用方法
2014/01/06 Javascript
javascript css红色经典选项卡效果实现代码
2016/05/17 Javascript
深入浅析JavaScript中的Function类型
2016/07/09 Javascript
Vue实现购物车功能
2017/04/27 Javascript
最通俗易懂的javascript变量提升详解
2017/08/05 Javascript
JavaScript实现二叉树的先序、中序及后序遍历方法详解
2017/10/26 Javascript
详解vuex之store拆分即多模块状态管理(modules)篇
2018/11/13 Javascript
浅谈layui分页控件field参数接收对象的问题
2019/09/20 Javascript
原生js拖拽实现图形伸缩效果
2020/02/10 Javascript
[02:00]最后,我终于出了辉耀
2018/03/27 DOTA
[01:01:36]Optic vs paiN 2018国际邀请赛小组赛BO2 第一场 8.19
2018/08/21 DOTA
[56:42]完美世界DOTA2联赛循环赛 Matador vs Forest 第二场 11.06
2020/11/06 DOTA
Python记录详细调用堆栈日志的方法
2015/05/05 Python
Python3如何对urllib和urllib2进行重构
2019/11/25 Python
关于初始种子自动选取的区域生长实例(python+opencv)
2020/01/16 Python
PageFactory设计模式基于python实现
2020/04/14 Python
pycharm 添加解释器的方法步骤
2020/08/31 Python
python 实现客户端与服务端的通信
2020/12/23 Python
CSS3弹性盒模型flex box快速入门心得(必看篇)
2016/05/24 HTML / CSS
HTML5 绘制图像(上)之:关于canvas元素引领下一代web页面的问题
2013/04/24 HTML / CSS
5分钟实现Canvas鼠标跟随动画背景
2019/11/18 HTML / CSS
Mountain Warehouse波兰官方网站:英国户外品牌
2019/08/29 全球购物
乡镇干部十八大感言
2014/02/17 职场文书
学习群众路线的心得体会
2014/11/05 职场文书
2014年学校体育工作总结
2014/12/08 职场文书
物流仓管员岗位职责
2015/04/01 职场文书
正规借条模板
2015/05/26 职场文书
深入理解go slice结构
2021/09/15 Golang
mysql使用 not int 子查询隐含陷阱
2022/04/12 MySQL