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 相关文章推荐
JavaScript编程的10个实用小技巧
Apr 18 Javascript
jQuery中children()方法用法实例
Jan 07 Javascript
js实现搜索框关键字智能匹配代码
Mar 26 Javascript
手机端图片缩放旋转全屏查看PhotoSwipe.js插件实现
Aug 25 Javascript
深入了解JavaScript的逻辑运算符(与、或)
Dec 20 Javascript
详解angular中通过$location获取路径(参数)的写法
Mar 21 Javascript
浅谈Vue.js 1.x 和 2.x 实例的生命周期
Jul 25 Javascript
微信小程序自定义tab实现多层tab嵌套功能
Jun 15 Javascript
js实现文件上传功能 后台使用MultipartFile
Sep 08 Javascript
前端面试知识点目录一览
Apr 15 Javascript
Node使用Nodemailer发送邮件的方法实现
Feb 24 Javascript
vue自定义右键菜单之全局实现
Apr 09 Vue.js
用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
Yii操作数据库的3种方法
2014/03/11 PHP
PHP连接MySQL的2种方法小结以及防止乱码
2014/03/11 PHP
PHP会话控制:Session与Cookie详解
2014/09/27 PHP
使用PHP Socket 编程模拟Http post和get请求
2014/11/25 PHP
PHP删除指定目录中的所有目录及文件的方法
2015/02/26 PHP
laravel 5 实现模板主题功能(续)
2015/03/02 PHP
通过ifame指向的页面高度调整iframe的高度
2006/10/05 Javascript
使用jQuery全局事件ajaxStart为特定请求实现提示效果的代码
2010/12/30 Javascript
jquery监控数据是否变化(修正版)
2011/04/12 Javascript
JS 实现导航栏悬停效果(续)
2013/09/24 Javascript
JsRender for index循环索引用法详解
2014/10/31 Javascript
js识别uc浏览器的代码
2015/11/06 Javascript
使用Node.js处理前端代码文件的编码问题
2016/02/16 Javascript
大型JavaScript应用程序架构设计模式
2016/06/29 Javascript
JS查找数组中重复元素的方法详解
2017/06/14 Javascript
关于javascript sort()排序你可能忽略的一点理解
2017/07/18 Javascript
JavaScript中利用Array filter() 方法压缩稀疏数组
2018/02/24 Javascript
Vue2.0中集成UEditor富文本编辑器的方法
2018/03/03 Javascript
javascript实现文件拖拽事件
2018/03/29 Javascript
详解JavaScript栈内存与堆内存
2019/04/04 Javascript
jQuery实现的图片点击放大缩小功能案例
2020/01/02 jQuery
跟老齐学Python之不要红头文件(1)
2014/09/28 Python
Windows下PyMongo下载及安装教程
2015/04/27 Python
详谈Python中列表list,元祖tuple和numpy中的array区别
2018/04/18 Python
详解Python 4.0 预计推出的新功能
2019/07/26 Python
浅谈keras中的目标函数和优化函数MSE用法
2020/06/10 Python
使用python编写一个语音朗读闹钟功能的示例代码
2020/07/14 Python
Python txt文件常用读写操作代码实例
2020/08/03 Python
英国时尚家具、家居饰品及礼品商店:Graham & Green
2016/09/15 全球购物
捷克多品牌在线时尚商店:ANSWEAR.cz
2020/10/03 全球购物
ASICS印度官方网站:日本专业运动品牌
2020/06/20 全球购物
简述你对Statement,PreparedStatement,CallableStatement的理解
2013/03/25 面试题
ktv收银员岗位职责
2013/12/16 职场文书
法院先进个人事迹材料
2014/05/04 职场文书
大学生创业计划书怎么写
2014/09/15 职场文书
mysql的Buffer Pool存储及原理
2022/04/02 MySQL