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 相关文章推荐
jQuery html() in Firefox (uses .innerHTML) ignores DOM changes
Mar 05 Javascript
JavaScript NodeTree导航栏(菜单项JSON类型/自制)
Feb 01 Javascript
Javascript中的apply()方法浅析
Mar 15 Javascript
HTML Table 空白单元格补全的简单实现
Oct 13 Javascript
基于jQuery实现表格的排序
Dec 02 Javascript
js实现刷新页面后回到记录时滚动条的位置【两种方案可选】
Dec 12 Javascript
Angular2库初探
Mar 01 Javascript
JS实现预加载视频音频/视频获取截图(返回canvas截图)
Oct 09 Javascript
在 webpack 中使用 ECharts的实例详解
Feb 05 Javascript
小程序实现页面顶部选项卡效果
Nov 06 Javascript
关于微信小程序获取小程序码并接受buffer流保存为图片的方法
Jun 07 Javascript
使用layer.msg 时间设置不起作用的解决方法
Sep 12 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函数,php爱好者站推荐
2007/03/19 PHP
PHP __autoload函数(自动载入类文件)的使用方法
2012/02/04 PHP
PHP简单读取PDF页数的实现方法
2016/07/21 PHP
php中通用的excel导出方法实例
2017/12/30 PHP
传智播客学习之JavaScript基础篇
2009/11/13 Javascript
window.js 主要包含了页面的一些操作
2009/12/23 Javascript
jquery 实现checkbox全选,反选,全不选等功能代码(奇数)
2012/10/24 Javascript
table对象中的insertRow与deleteRow使用示例
2014/01/26 Javascript
模拟一个类似百度google的模糊搜索下拉列表
2014/04/15 Javascript
JavaScript学习笔记之Cookie对象
2015/01/22 Javascript
原生js实现类似弹窗抖动效果
2015/04/02 Javascript
详解vue2.6插槽更新v-slot用法总结
2019/03/09 Javascript
js console.log打印对象时属性缺失的解决方法
2019/05/23 Javascript
layui的select联动实现代码
2019/09/28 Javascript
[02:44]重置世界,颠覆未来——DOTA2 7.23版本震撼上线
2019/12/01 DOTA
Python的加密模块md5、sha、crypt使用实例
2014/09/28 Python
Python实现中文数字转换为阿拉伯数字的方法示例
2017/05/26 Python
Python实现自动上京东抢手机
2018/02/06 Python
浅谈Python2、Python3相对路径、绝对路径导入方法
2018/06/22 Python
python输入整条数据分割存入数组的方法
2018/11/13 Python
使用python实现抓取腾讯视频所有电影的爬虫
2019/04/15 Python
pytorch 状态字典:state_dict使用详解
2020/01/17 Python
pycharm不能运行.py文件的解决方法
2020/02/12 Python
Python中bisect的用法及示例详解
2020/07/20 Python
html5 worker 实例(二) 图片变换效果
2013/06/24 HTML / CSS
质检部经理岗位职责
2014/02/19 职场文书
贺卡寄语大全
2014/04/11 职场文书
文艺晚会策划方案
2014/06/11 职场文书
护士节演讲稿开场白
2014/08/25 职场文书
搞笑老公保证书
2015/02/26 职场文书
现场施工员岗位职责
2015/04/11 职场文书
地球上的星星观后感
2015/06/02 职场文书
小学生反邪教心得体会
2016/01/15 职场文书
干货!开幕词的写作方法
2019/04/02 职场文书
简短的人生哲理(38句)
2019/08/13 职场文书
高中班主任工作总结(范文)
2019/08/20 职场文书