javascript 从if else 到 switch case 再到抽象


Posted in Javascript onJuly 17, 2010

我的答案是,超过两个 else 的 if ,或者是超过两个 case 的 switch 。可是在代码中大量使用 if else 和 switch case 是很正常的事情吧?错!绝大多数分支超过两个的 if else 和 switch case 都不应该以硬编码( hard-coded )的形式出现。
复杂分支从何而来
首先我们要讨论的第一个问题是,为什么遗留代码里面往往有那么多复杂分支。这些复杂分支在代码的首个版本中往往是不存在的,假设做设计的人还是有点经验的话,他应该预见将来可能需要进行扩展的地方,并且预留抽象接口。

但是代码经过若干个版本的迭代以后,尤其是经过若干次需求细节的调整以后,复杂分支就会出现了。需求的细节调整,往往不会反映到 UML 上,而会直接反映到代码上。例如说,原本消息分为聊天消息和系统消息两类,设计的时候自然会把这设计为消息类的两个子类。但接着有一天需求发生细节调整了,系统消息里面有一部分是重要的,它们的标题要显示为红色,这时候程序员往往会做如下修改:
在系统消息类上面加一个 important 属性
在相应的 render 方法里面加入一个关于 important 属性的分支,用于控制标题颜色
程序员为什么会作出这样的修改?有可能因为他没意识到应该抽象。因为需求说的是「系统消息里面有一部分是重要的」,对于接受命令式编程语言训练比较多的程序员来说,他或许首先想到的是标志位──一个标志位就可以区分重要跟不重要。他没想到这个需求可以用另一种方式来解读,「系统消息分为重要和不重要两种类别」。这样子解读,他就知道应该对系统消息进行抽象了。
当然也有可能,程序员知道可以抽象,但基于某些原因,他选择了不这样做。很常见的一种情况就是有人逼着程序员,以牺牲代码质量来换取项目进展速度──加入一个属性和一个分支,远比抽象重构要简单得多,如果要做10个这种形式的修改,是做10个分支快还是做10个抽象快?区别显而易见。
当然, if else 多了,就有聪明人站出来说「不如我们改成 switch case 」吧。在某些情况下,这确实能够提升代码可读性,假设每一个分支都是互斥的话。但是当 switch case 的数量也多起来以后,代码一样会变得不可读。
复杂分支有何坏处
复杂分支有什么坏处?让我从百度 Hi 网页版的老代码里面截取一段出来做个例子。

switch (json.result) { 
case "ok": 
switch (json.command) { 
case "message": 
case "systemmessage": 
if (json.content.from == "" 
&& json.content.content == "kicked") { 
/* disconnect */ 
} else if (json.command == "systemmessage" 
|| json.content.type == "sysmsg") { 
/* render system message */ 
} else { 
/* render chat message */ 
} 
break; 
} 
break;

这段代码要看懂不难,因此我提一个简单问题,以下这个 JSON 命中哪个分支:
{ 
"result": "ok", 
"command": "message", 
"content": { 
"from": "CatChen", 
"content": "Hello!" 
} 
}

你很容易就能得到正确答案:这个 JSON 命中 /* render chat message */ (显示聊天消息)这个分支。那么我想了解一下,你是如何作出这个判断的?首先,你要看它是否命中 case "ok": 分支,结果是命中了;然后,你要看它是否命中 case "message": 分支,结果也是命中了,所以 case "systemmessage": 就不用看了;接下来,它不命中 if 里面的条件;并且,它也不命中 else if 里面的条件,所以它命中了 else 这个分支。
看出问题来了吗?为什么你不能看着这个 else 就说出这个 JSON 命中这个分支?因为 else 本身不包含任何条件,它只隐含条件!每一个 else 的条件,都是对它之前的每一个 if 和 else if 进行先非后与运算的结果。也就是说,判断命中这个 else ,相当于判断命中这样一组复杂的条件:
!(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")

再套上外层的两个 switch case ,这个分支的条件就是这样子的:
json.result == "ok" && (json.command == "message" || json.command == "systemmessage") && !(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")

这里面有重复逻辑,省略后是这样子的:
json.result == "ok" && json.command == "message" && !(json.content.from == "" && json.content.content == "kicked") && !(json.content.type == "sysmsg")

我们花了多大力气才从简简单单的 else 这四个字母中推导出这样一长串逻辑运算表达式来?况且,不仔细看还真的看不懂这个表达式说的是什么。
这就是复杂分支难以阅读和管理的地方。想象你面对一个 switch case 套一个 if else ,总共有3个 case ,每个 case 里面有3个 else ,这就够你研究的了──每一个分支,条件中都隐含着它所有前置分支以及所有祖先分支的前置分支先非后与的结果。
如何避免复杂分支
首先,复杂逻辑运算是不能避免的。重构得到的结果应该是等价的逻辑,我们能做的只是让代码变得更加容易阅读和管理。因此,我们的重点应该在于如何使得复杂逻辑运算变得易于阅读和管理。
抽象为类或者工厂
对于习惯于做面向对象设计的人来说,可能这意味着将复杂逻辑运算打散并分布到不同的类里面:
switch (json.result) { 
case "ok": 
var factory = commandFactories.getFactory(json.command); 
var command = factory.buildCommand(json); 
command.execute(); 
break; 
}

这看起来不错,至少分支变短了,代码变得容易阅读了。这个 switch case 只管状态码分支,对于 "ok" 这个状态码具体怎么处理,那是其他类管的事情。 getFactory 里面可能有一组分支,专注于创建这条指令应该选择哪一个工厂的选择。同时 buildCommand 可能又有另外一些琐碎的分支,决定如何构建这条指令。
这样做的好处是,分支之间的嵌套关系解除了,每一个分支只要在自己的上下文中保持正确就可以了。举个例子来说, getFactory 现在是一个具名函数,因此这个函数内的分支只要实现 getFactory 这个名字暗示的契约就可以了,无需关注实际调用 getFactory 的上下文。
抽象为模式匹配
另外一种做法,就是把这种复杂逻辑运算转述为模式匹配:
Network.listen({ 
"result": "ok", 
"command": "message", 
"content": { "from": "", "content": "kicked" } 
}, function(json) { /* disconnect */ }); 
Network.listen([{ 
"result": "ok", 
"command": "message", 
"content": { "type": "sysmsg" } 
}, { 
"result": "ok", 
"command": "systemmessage" 
}], function(json) { /* render system message */ }); 
Network.listen({ 
"result": "ok", 
"command": "message", 
"content": { "from$ne": "", "type$ne": "sysmsg" } 
}, function(json) { /* render chat message */ });

现在这样子是不是清晰多了?第一种情况,是被踢下线,必须匹配指定的 from 和 content 值。第二种情况,是显示系统消息,由于系统消息在两个版本的协议中略有不同,所以我们要捕捉两种不同的 JSON ,匹配任意一个都算是命中。第三种情况,是显示聊天消息,由于在老版本协议中系统消息和踢下线指令都属于特殊的聊天消息,为了兼容老版本协议,这两种情况要从显示聊天消息中排除出去,所以就使用了 "$ne" (表示 not equal )这样的后缀进行匹配。
由于 listen 方法是上下文无关的,每一个 listen 都独立声明自己匹配什么样的 JSON ,因此不存在任何隐含逻辑。例如说,要捕捉聊天消息,就必须显式声明排除 from == "" 以及 type == "sysmsg" 这两种情况,这不需要由上下文的 if else 推断得出。
使用模式匹配,可以大大提高代码的可读性和可维护性。由于我们要捕捉的是 JSON ,所以我们就使用 JSON 来描述每一个分支要捕捉什么,这比一个长长的逻辑运算表达式要清晰多了。同时在这个 JSON 上的每一处修改都是独立的,修改一个条件并不影响其他条件。
最后,如何编写一个这样的模式匹配模块,这已经超出了本文的范围。
Javascript 相关文章推荐
摘自启点的main.js
Apr 20 Javascript
jquery实现用户打分评分特效
May 28 Javascript
超实用的JavaScript代码段 附使用方法
May 22 Javascript
卸载安装Node.js与npm过程详解
Aug 15 Javascript
BootStrap 实现各种样式的进度条效果
Dec 07 Javascript
BootStrap学习系列之布局组件(下拉,按钮组[toolbar],上拉)
Jan 03 Javascript
jQuery选择器之表单元素选择器详解
Sep 19 jQuery
基于打包工具Webpack进行项目开发实例
May 29 Javascript
详解微信图片防盗链“此图片来自微信公众平台 未经允许不得引用”的解决方案
Apr 04 Javascript
bootstrap-table+treegrid实现树形表格
Jul 26 Javascript
解决vue单页面 回退页面 keeplive 缓存问题
Jul 22 Javascript
Vue中使用JsonView来展示Json树的实例代码
Nov 16 Javascript
用JavaScript对JSON进行模式匹配 (Part 2 - 实现)
Jul 17 #Javascript
用JavaScript对JSON进行模式匹配(Part 1-设计)
Jul 17 #Javascript
关于flash遮盖div浮动层的解决方法
Jul 17 #Javascript
JQUERY获取form表单值的代码
Jul 17 #Javascript
jQuery+ajax实现顶一下,踩一下效果
Jul 17 #Javascript
flexigrid 类似ext grid的JS表格代码
Jul 17 #Javascript
基于JQuery的Pager分页器实现代码
Jul 17 #Javascript
You might like
php+AJAX传送中文会导致乱码的问题的解决方法
2008/09/08 PHP
学习php设计模式 php实现策略模式(strategy)
2015/12/07 PHP
PHP面向对象程序设计子类扩展父类(子类重新载入父类)操作详解
2019/06/14 PHP
PHP7新特性
2021/03/09 PHP
prototype1.4中文手册
2006/09/22 Javascript
更换select下拉菜单背景样式的实现代码
2011/12/20 Javascript
JavaScript中自定义事件用法分析
2014/12/23 Javascript
AngularJs页面筛选标签小功能
2016/08/01 Javascript
解决ie img标签内存泄漏的问题
2017/10/13 Javascript
微信小程序上传图片到服务器实例代码
2017/11/07 Javascript
浅谈VUE监听窗口变化事件的问题
2018/02/24 Javascript
JavaScript引用类型RegExp基本用法详解
2018/08/09 Javascript
详解webpack 热更新优化
2018/09/13 Javascript
对node通过fs模块判断文件是否是文件夹的实例讲解
2019/06/10 Javascript
微信小程序下拉加载和上拉刷新两种实现方法详解
2019/09/05 Javascript
微信小程序获取用户信息及手机号(后端TP5.0)
2019/09/12 Javascript
nodejs对mongodb数据库的增加修删该查实例代码
2020/01/05 NodeJs
django之session与分页(实例讲解)
2017/11/13 Python
pandas 透视表中文字段排序方法
2018/11/16 Python
Python3爬虫学习之爬虫利器Beautiful Soup用法分析
2018/12/12 Python
python将txt文档每行内容循环插入数据库的方法
2018/12/28 Python
使用Django2快速开发Web项目的详细步骤
2019/01/06 Python
Python实现将HTML转成PDF的方法分析
2019/05/04 Python
浅析Python 中几种字符串格式化方法及其比较
2019/07/02 Python
Python内存映射文件读写方式
2020/04/24 Python
Python ckeditor富文本编辑器代码实例解析
2020/06/22 Python
CSS书写规范、顺序和命名规则
2014/03/06 HTML / CSS
京东国际站:JOYBUY
2017/11/23 全球购物
linux面试题参考答案(7)
2014/07/24 面试题
Python如何实现单例模式
2016/06/03 面试题
销售岗位职责范本
2014/06/12 职场文书
人力资源职位说明书
2014/07/29 职场文书
行风评议整改报告
2014/11/06 职场文书
妈妈别哭观后感
2015/06/08 职场文书
如何让vue长列表快速加载
2021/03/29 Vue.js
pandas中关于apply+lambda的应用
2022/02/28 Python