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 相关文章推荐
浅谈js中的闭包
Mar 16 Javascript
微信小程序实战之自定义toast(6)
Apr 18 Javascript
Angular 4 依赖注入学习教程之FactoryProvider的使用(四)
Jun 04 Javascript
js+html5实现侧滑页面效果
Jul 15 Javascript
利用纯JS实现像素逐渐显示的方法示例
Aug 14 Javascript
Node.js+jade抓取博客所有文章生成静态html文件的实例
Sep 19 Javascript
jQuery 禁止表单用户名、密码自动填充功能
Oct 30 jQuery
vue自定义过滤器创建和使用方法详解
Nov 06 Javascript
浅谈vue,angular,react数据双向绑定原理分析
Nov 28 Javascript
HTML+JavaScript实现扫雷小游戏
Sep 30 Javascript
微信小程序仿抖音短视频切换效果的实例代码
Jun 24 Javascript
js仿淘宝放大镜效果
Dec 28 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
使用数据库保存session的方法
2006/10/09 PHP
PC端微信扫码支付成功之后自动跳转php版代码
2017/07/07 PHP
PHP观察者模式示例【Laravel框架中有用到】
2018/06/15 PHP
PHP实现字符串大小写转函数的功能实例
2019/02/06 PHP
使用jquery与图片美化checkbox和radio控件的代码(打包下载)
2010/11/11 Javascript
dojo随手记 gird组件引用
2011/02/24 Javascript
解决jquery submit()提交表单提示:f[s] is not a function
2013/01/23 Javascript
jquery图片滚动放大代码分享(2)
2015/08/28 Javascript
实例讲解JavaScript中的this指向错误解决方法
2016/06/13 Javascript
JS中检测数据类型的几种方式及优缺点小结
2016/12/12 Javascript
深入浅出webpack之externals的使用
2017/12/04 Javascript
JavaScript体验异步更好的解决办法
2018/01/08 Javascript
vue基于element-ui的三级CheckBox复选框功能的实现代码
2018/10/15 Javascript
JavaScript实现的3D旋转魔方动画效果实例代码
2019/07/31 Javascript
解决父组件将子组件作为弹窗调用只执行一次created的问题
2020/07/24 Javascript
[01:31]DOTA2上海特级锦标赛 SECRET战队完整宣传片
2016/03/16 DOTA
[01:51]历届DOTA2国际邀请赛举办地回顾 TI9落地上海
2018/08/26 DOTA
python计算一个序列的平均值的方法
2015/07/11 Python
Python序列操作之进阶篇
2016/12/08 Python
Python 文件操作的详解及实例
2017/09/18 Python
Python基于更相减损术实现求解最大公约数的方法
2018/04/04 Python
Python3 安装PyQt5及exe打包图文教程
2019/01/08 Python
Python中判断子串存在的性能比较及分析总结
2019/06/23 Python
Python字典对象实现原理详解
2019/07/01 Python
Python二维码生成识别实例详解
2019/07/16 Python
韩国三星集团旗下时尚品牌官网:SSF SHOP
2016/08/02 全球购物
阿尔卡特(中国)的面试题目
2014/08/20 面试题
便利店投资的创业计划书
2014/01/12 职场文书
给物业的表扬信
2014/01/21 职场文书
学校政风行风评议工作总结
2014/10/21 职场文书
2014学习十八届四中全会精神思想汇报范文
2014/10/23 职场文书
2015年计算机教学工作总结
2015/07/22 职场文书
唱歌比赛拉拉队口号
2015/12/25 职场文书
导游词之徐州-云龙山
2019/09/29 职场文书
Go 实现英尺和米的简单单位换算方式
2021/04/29 Golang
JavaScript 数组去重详解
2021/09/15 Javascript