代码重构(四):条件表达式重构规则
继续更新有关重构的博客,前三篇是关于类、函数和数据的重构的博客,内容还算比较 充实吧。今天继续更新,本篇博客的主题是关于条件表达式的重构规则。有时候在实现比较复杂的业务逻辑时,各种条件各种嵌套。如果处理不好的话,代码看上去 会非常的糟糕,而且业务逻辑看上去会非常混乱。今天就通过一些重构规则来对条件表达式进行重构,让业务逻辑更为清晰,代码更以维护和扩展。
今天博客中的代码示例依然是Swift班,在对条件表达式重构时也会提现出Swift的优雅之处,会用上Swift特有的语法及其特点,比如使用 guard来取代if-let语句等。如果你的需求的业务逻辑及其复杂,那么妥善处理条件表达式尤为重要。因为对其妥善处理可以提高代码的可读性,以及提 高代码的可维护性。说这么多还是来些示例来的直观,下方会根据一些Demo来着重分享一些条件表达式的部分重构规则,当然今天博客中没有涵盖所有的条件表 达式的重构规则,更详细的部分请参见经典的重构书籍。
今天所分享的代码段也将会在github上进行分享,分享地址在本篇博文的后方。废话少说了,进入今天的主题。
目录:
一、分解条件表达式
二、合并条件表达式
三、合并重复的条件片段
四、移除控制标记
五、以卫语句取代嵌套的条件
六、使用多态取代条件表达式
一.Decompose Conditional(分解条件表达式)
顾名思义,分解条件表达式说白了,就是 当你的条件表达式比较复杂时,你就可以对其进行拆分。一般拆分的规则为:经if后的复杂条件表达式进行提取,将其封装成函数。如果if与else语句块中 的内容比较复杂,那么就将其提取,也封装成独立的函数,然后在相应的地方进行替换。
下方代码段就是我们将要重构的代码段。 因为本篇博客的主题是对条件表达式的重构,所以我们要对象下方的if-else的代码块进行重构。至于下方代码片段中其他不规范以及需要重构的地方我们暂 且忽略。因为我们本篇博客的主题是条件表达式的重构。接下来我们就要对下方代码片段中的条件表达式进行分析了。因为下方这段代码毕竟是一个Demo,在这 儿我们可以做个假设,假设if后边的表达式比较复杂,然后在if语句块和else语句块中都有一些复杂的处理,代码看上去的大体样子如下所示。
基于对上述代码的结构的假设,接下来我 们将要对其进行重构。说白了,就是让将条件表达式中的比较复杂的模块进行拆分与提取。下方代码段就是我们重构后的结构,就是将我们假设比较复杂的模块进行 封装,然后在条件表达式中使用函数进行替换。这样的话,在看条件表达式就比较清晰。当然,我们这个Demo的条件表达式不够复杂,并且if和else的逻 辑块所做的东西不多。不过我们可以假设一下,如果在比较复杂的情况下,这种重构手法是比较实用的。具体的大家就看重构前与重构后的区别吧。
二、Consolidate Conditional Expression(合并条件表达式)
“合并条件表达式”这条规则也是比较好理解的,因为有时候会存在这样的情况,也就是一些条件表达式后的语句体执行的代码块相同。说白了也就是不同的 条件有着同样的返回结果。当然一般在你程序设计之初不会出现此问题,因为在我们设计程序时,如果不同的条件返回相同的结果,我们肯定会将其合并的。不过当 你在多个版本迭代,多个需求要增加,或者在别人的代码上进行需求迭代的时候,该情况是很有可能发生的。
说这么多,也许有些抽象,那么就直接看下方需要重构的Demo了。当然,下方的Demo中,我们为了测试,其中的条件比较简单。我们假设每个条件表达式是在不同的需求迭代中或者修改bug时添加的,从而造成了下方这种情况(当然下方的情况有些夸张,这也是为了突出要合并条件的情况)。
在上述夸张的Demo中一眼就能看出来如何进行重构了(在日常开发迭代中,因为业务逻辑的复杂性或者多次迭代的原因,往往不是那么一目了然)。接下 来我们就要对不同条件,但返回相同结果的部分进行合并。下方就是我们合并后的结果,重构手法就是讲不同的条件表达式使用&&或者||等布 尔运算进行合并。
合并后,如果条件比较复杂,那么我们就可以使用本片博客中的第一部分使用的重构规则进行再次重构。下方代码段是进行第二次重构,就是对比较复杂的表 达式进行函数封装,具体如下所示。还是那句话,Demo有些夸张,不过用来演示该重构规则也是不错的,思想就这个思想,具体在日常开发中的使用场景还是需 要进行琢磨和推敲的。
三、Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
第二部分合并的是条件表达式,本部分是 合并的是重复的条件片段。什么叫合并重复的条件片段呢?这种情况也是一般不会在设计程序之初所出现,但是随着时间的推移,项目不断迭代更新,或者需求变更 和迭代更新等等,在项目后期维护时比较容易出现重复的条件片段。在开发中是比较忌讳重复的代码的,如果出现重复的代码,那么说明你的代码应该被重构了。
下方代码片段中if与else中有着相 同的语句,就是这个print语句。当然这个示例也是比较夸张的,但是足以说明问题。如果你在开发业务逻辑比较复杂的条件表达式时,要谨慎的检查一下有没 有下方这种情况。也就是出现了重复的条件片段。这种情况在需求迭代或者变更中是及其容易出现的。当然下方只是我们这儿列举的一个夸张的示例。
对于这个示例而言,我们不难看出,去代码的重复化。将print语句移到条件之外。但是要学会举一反三呢,重要的是重构手法和思想。在真正的项目 中,如果你要提取重复的代码段一般还要结合着其他重构手法,比如将重复的部分先提取成一个独立的模块(独立的类或者方法),然后在条件中使用,最后再去重 复话。这样一来,重构的思路就比较清晰了。虽然今天的示例比较简单,但是足以表达这个思路。下方是重构后的代码。如果你对下方代码看着不爽的话,完全可以 根据之前我们介绍的重构手法“使用查询来替代临时变量”,将下方的代码继续重构,在本章博客中就不做过多介绍了。
四、Remove Control Flag(移除控制标记)
“移除控制标记”这一点还是比较重要 的,我平时在代码开发中有时候也会使用到标记变量,来标记一些事物的状态。使用标记变量最直观的感受就是不易维护,不易理解。因为在需求变更或者迭代中, 你还得维护这标记变量。如果维护一个标记变量简单的话,那么维护多个标记变量就没这么容易了。而且在你的程序中使用标记变量时,不易理解,并且会显得逻辑 混乱。当然这是我的直观感受,在写程序时,我尽量会避免使用标记变量。
当然,下方又是一个有点夸张的例子,但是该例子可以说明问题。下方代码中我们使用了一个flag标记变量,当然下方代码没有什么意义了。在平时开发 中我们会使用一些标记变量来标记一个或者一些数据的状态,或者一些控件的状态,再次为了简化示例,我们就简单的引入了一个flag标记变量。下方代码不难 理解,当i为20时,我们就翻转标记变量的状态,然后if中的语句块就不被执行了。
虽然下方代码片段是我写的,但是我个人看着超级的不舒服。引入的这个flag增加了代码的逻辑复杂度,让代码变得不那么直观。我个人建议,在平时开发中尽量的要少使用标记变量。不到万不得已,不要在你的代码中引入标记变量。如果有,尝试着去除标记变量。
标记变量一般是可以使用其他语句进行替换的,可以使用break、return、continue等等,这个要根据具体情况而定。总之,代码中有标 记变量不是什么好的事情。下方代码段就是对上述代码去除标记变量的重构。重构后的代码如下所示,当然还有好多其他去除的方法,此处仅仅给出了一种。
五、Replace Nested Condition with Guard Clauses(以卫语句取代嵌套的条件)
条件表达式的嵌套是令人讨厌的东西。代码中有多层if-else嵌套会降低代码的可读性以及可维护性,如果此时在加上for循环等等其他逻辑语句, 想想都可怕。这种业务逻辑较强的代码要慎重对待。尽量不要将if-else进行嵌套,因为嵌套的if-else确实不好理解,如果在出现bug时,更是不 好定位bug。要记住,你写的代码不是给机器看的,而是给人看的,这一点非常重要。不光是代码编写规范,也尽量不要使用理解起来比较费劲的语句来实现你的 逻辑。
下方我们将创建一种场景,人为的创建多个if嵌套的情况。下方的demo理解起来应该不难,第一个数组中存储的是第二个字典的key,第二个字典中 存储的value是下一个字典也就是第三个字典的key,以此类推。将我们在使用从相应的字典中取出的value做为key再次取值时,我们要保证该值不 为nil,所以我们要进行if-let判断。if-let所表示的意思是在取值时,如果当前取出的值不为nil,那么就执行if后的语句体,如果为 nil,那么就不执行。这样一来,就会出现多层if-let嵌套的情况。
当然,在一些业务逻辑比较复杂的需求中,嵌套的每层if后都跟着不同的表达式,而不仅仅是if-let。因为为了创建这个if嵌套的场景,再次我们使用了if-let嵌套。这么多的if-let嵌套显然不是什么好的事情,所以我们要对此重构。
如果多层if嵌套,会出现一种叫做“厄运金字塔”的现象,因为在if左边会出现一个三角号的空间。这可不是什么好的标志,这样的代码结构一般理解起 来会比较困难,维护起来也不是那么的理想。所以下方我们要对上述代码进行结构。要去除上面的嵌套模式,我们可以将if后的条件进行翻转,根据具体需求再引 入return、break、continue等卫语句。下方是讲条件进行翻转然后引入了continue语句,代码如下:
该部分的第二段代码要比第一段代码容易理解的多。经过条件翻转+continue,将上述嵌套的条件语句进行了拆分。拆分成了三个独立的if语句, 虽然代码结构不同,但是其实现功能都是一样的。不过上面的解决方案在Swift中并不完美。因为Swift语言是非常优雅的,Swift语言在设计的时候 就考虑到了这种情况,所以在Swift 2.0时推出了guard语句。在这种情况下使用guard语句再合适不过了,下方代码段就是使用guard语句进行了重构。
使用guard let声明的变量与guard本身同在一个作用域,也就是说下方代码在guard let中声明的变量可以在for循环中直接使用。guard语句的用法就是如果guard 后方的赋值语句所取出的值为nil,那么就会执行else中的语句,否则就会继续往下执行。在else中一般是break、return、 continue等卫语句。这种语法形式很好的对上述糟糕的形式进行了解决,而且还易于理解。
六、Replace Condition with Polymorphism(以多态取代条件表达式)
在介绍“以多态取代条件表达式” 之前呢,首先要理解面向对象中多态是什么,也就是说多态是干嘛的。顾明思议,多态就是类的不同类型的对象有着不同的行为状态。如果在你的条件表达式中条件 是对象的类型,也就是根据对象的不同类型然后做不同的事情。在这种情况下使用多态在合适不过了。如果该部分在设计模式中,应该对应着状态模式这一部分。这就是以多态来取代条件表达式。
下方是一个比较简单的示例,这也正是我 们要进行重构的示例。在Book类中有三中类型,也就是我们的书有三种,具体每种书是什么这不是该示例的重点。在Book类实例化时,需要为书的对象指定 该书的类型(三种类型中的一种)。在Book类中,还有一个核心方法,那就是计算书的价格。在charge()函数中,根据不同的书的种类,给出了不同的 价格。当然在Switch中的分支的计算方法在本例中非常简单,但是我们要假设每个分支的计算非常复杂,而且有着多行代码。
在这种假设的情况下,下方的条件语句是非常糟糕的,因为庞大的业务逻辑增加了代码维护的成本。在这种情况下我们就可以使用多态来取代复杂的条件表达式。
如果想使用多态,引入其他类是必不可少的,而且每个类中也必须有相应的对应关系。“以多态取代条件表达式”的做法的本质是将不同状态的业务逻辑的处 理的代码移到相应的类中。在本示例中,我们要创建三种书籍的价格类,并且将上述case中的“复杂”计算移入到相应的书籍类中。因为每个书籍价格中都会有 相应的计算方法,也就是charge()方法,所以我们为这三个书籍价格定义了一个协议(接口或者抽象类),在协议中就给出了charge()函数。然后 我们就可以将不同种类的书籍实现该协议,在相应的方法中给出价格计算的代码。具体做法如下所示:
引入上述几个类后,在我们的Book中就可以使用多态了。在Book类中添加了一个price字段,这个字段的类型就是我们的Price协议。也就 是只要是符合我们的Price协议的对象都可以。然后在Book中也添加了一个charge()方法,在Book中的charge方法做的一件事情就是调 用price对象的charge方法。关键的是根据不同的书籍类型创建不同的书籍价格对象。这样一来,我们就把每个分支中的业务逻辑进行了分离,并使用了多态来获取价格。重构后的优点不言而喻。
今天关于“条件表达式的重构”的规则,当然这不是全部的,只是列举了一些常见的,而且经常使用重构规则。篇幅有限,今天的博客就先到这儿,还会继续更新其他的重构规则。
今天博客中的Demo在github上的分享地址为:https://github.com/lizelu/CodeRefactoring-Swift