从 if else 到 switch case 再到抽象

📅 2026/7/6 14:25:05
从 if else 到 switch case 再到抽象
大家觉得在接手遗留代码时见到什么东东是最让人感到不耐烦的复杂无比的 UML 我觉得不是。我的答案是超过两个 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 }}, func tion(json) { /* render chat message */ });现在这样子是不是清晰多了第一种情 况是被踢下线必须匹配指定的from和content值。第二种情况是显示系统消息由于系统消息在两个版本的协议中略有不同所以我们要捕捉两种不同的 JSON 匹配任意一个都算是命中。第三种情况是显示聊天消息由于在老版本协议中系统消息和踢下线指令都属于特殊的聊天消息为了兼容老版本协议这两种情况 要从显示聊天消息中排除出去所以就使用了$ne表示 not equal 这样的后缀进行匹配。由于listen方法是上下文无关的每一个listen都独立声明自己匹配什么样的 JSON 因此不存在任何隐含逻辑。例如说要捕捉聊天消息就必须显式声明排除from 以及type sysmsg这两种情况这不需要由上下文的 if else 推断得出。使用模式匹配可以大大提高代码的可读性和可维护性。由于我们要捕捉的是 JSON 所以我们就使用 JSON 来描述每一个分支要捕捉什么这比一个长长的逻辑运算表达式要清晰多了。同时在这个 JSON 上的每一处修改都是独立的修改一个条件并不影响其他条件。最后如何编写一个这样的模式匹配模块这已 经超出了本文的范围。如果你感兴趣你可以订阅我的博客我会在将来写文章描述这个模块Cat in dotNET (feed)