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 ul标签下拉菜单演示代码
Dec 11 Javascript
js 实现的可折叠留言板(附源码下载)
Jul 01 Javascript
简介AngularJS的视图功能应用
Jun 17 Javascript
url传递的参数值中包含&时,url自动截断问题的解决方法
Aug 02 Javascript
AngularJS ng-repeat数组有重复值的解决方法
Oct 23 Javascript
webpack 2.x配置reactjs基本开发环境详解
Aug 08 Javascript
Vue2.5通过json文件读取数据的方法
Feb 27 Javascript
vue addRoutes实现动态权限路由菜单的示例
May 15 Javascript
解决angularjs中同步执行http请求的方法
Aug 13 Javascript
layui清除radio的选中状态实例
Nov 14 Javascript
微信小程序实现列表滚动头部吸顶的示例代码
Jul 12 Javascript
vue 函数调用加括号与不加括号的区别
Oct 29 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也可以?成Shell Script
2006/10/09 PHP
ThinkPHP控制器间实现相互调用的方法
2014/10/31 PHP
php导入模块文件分享
2015/03/17 PHP
fckeditor上传文件按日期存放及重命名方法
2015/05/22 PHP
AngularJS基础 ng-click 指令示例代码
2016/08/01 Javascript
js倒计时简单实现代码
2016/08/11 Javascript
js制作网站首页图片轮播特效代码
2016/08/30 Javascript
Vue.JS入门教程之事件监听
2016/12/01 Javascript
微信小程序开发教程-手势解锁实例
2017/01/06 Javascript
jQuery中Datatables增加跳转到指定页功能
2017/02/08 Javascript
Bootstrap实现各种进度条样式详解
2017/04/13 Javascript
React根据宽度自适应高度的示例代码
2017/10/11 Javascript
Node解决简单重复问题系列之Excel内容的获取
2018/01/02 Javascript
微信小程序定义和调用全局变量globalData的实现
2019/11/01 Javascript
javascript实现固定侧边栏
2021/02/09 Javascript
[39:11]DOTA2上海特级锦标赛C组资格赛#2 LGD VS Newbee第二局
2016/02/28 DOTA
[02:04]完美世界城市挑战赛秋季赛报名开始 谁是solo路人王?
2019/10/10 DOTA
理解Python中的绝对路径和相对路径
2017/08/30 Python
详谈python3中用for循环删除列表中元素的坑
2018/04/19 Python
对python中矩阵相加函数sum()的使用详解
2019/01/28 Python
django中ORM模型常用的字段的使用方法
2019/03/05 Python
搭建python django虚拟环境完整步骤详解
2019/07/08 Python
Python守护进程实现过程详解
2020/02/10 Python
Python终端输出彩色字符方法详解
2020/02/11 Python
如何快速理解python的垃圾回收机制
2020/09/01 Python
python 获取字典键值对的实现
2020/11/12 Python
文化活动实施方案
2014/03/28 职场文书
工厂门卫岗位职责范本
2014/04/04 职场文书
纪念九一八事变演讲稿:牢记历史,捍卫主权
2014/09/14 职场文书
乡镇干部党的群众路线教育实践活动个人对照检查材料
2014/09/24 职场文书
教师批评与自我批评发言稿
2014/10/15 职场文书
2014旅游局党组书记党建工作汇报材料
2014/11/02 职场文书
小班下学期幼儿评语
2014/12/30 职场文书
个人总结与自我评价
2015/02/14 职场文书
家长对孩子的寒假评语
2015/10/09 职场文书
《黄道婆》教学反思
2016/02/22 职场文书