代码重构(一):函数重构规则
重构是项目做到一定程度后必然要做的事情。代码重构,可以改善既有的代码设计,增强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,我们在项目中所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但这些功能模块有可能在下一次需求中不在适用。或者你因为需求迭代与变更,使你原有的方法或者类变得臃肿,以及各个模块或者层次之间耦合度增加。此时,你要考虑重构了。
重构,在《重构,改善既有代码的设计》这本经典的书中给出了定义,大概就是:在不改变代码对外的表现的情况下,修改代码的内部特征。说白了,就是我们的测试用例不变,然后我们对既有的代码的结构进行修改。重构在软件开发中是经常遇到的,也是非常重要的。在需求迭代,Debug,Code Review时,你都可以对你既有的代码进行重构。
在接下来的几篇博文中,我想与大家一块去窥探一下代码重构的美丽,学习一下代码重构的一些规则。当然在每个规则中都有小的Demo, 在本篇博客以及相关内容的博客是使用Swift语言实现的。当然,和设计模式相同,重构重要的是手法和思想,和使用什么样的语言关系不大。经典的重构书籍中是使用Java语言来实现的,如果你对PHP, Python等其他语言比较熟悉,完全可以使用这些语言来测试一些重构手法。
本篇博客的主题就是通过一些列的重构手法,对既有的需要重构的函数或者方法进行重构。并且会将每个示例在GitHub上进行分享,感兴趣的小伙伴可以对其进行下载。有的小伙伴说了,我没有Mac,怎么对你写的Swift代码进行编译呢?这个问题好解决,你可以看我之前发表的这篇博客《窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型》。你可以将相关代码进行拷贝,在浏览器中观察结果。因为在线编译的网站是国外的,访问起来也许会有一些卡顿,不过是可以用的。好前面扯了这么多了,进入今天的主题。
一、Extract Method(提取函数)-------将大函数按模块拆分成几个小的函数
Extract Method被翻译成中文就是提取函数的意思,这一点在代码重构中用的非常非常的多。在重构时提倡将代码模块进行细分,因为模块越小,可重用度就越大。不要写大函数,如果你的函数过大,那么这意味着你的函数需要重构了。因为函数过大,可维护性,可理解性就会变差。并且当你实现类似功能的时候就容易产生重复代码。写代码时,最忌讳的就是代码重复。这也就是经常所说的DRY(Don`t Repeat Yourself)原则。所以当函数过长时,你需要将其细分,将原函数拆分成几个函数。
下方将会通过一个示例来直观的感受一下Extract Method,当然这些示例不是我原创的,是《重构:改善既有代码的设计》中Java示例演变的Swift版,在写Swift代码时,对原有的示例进行了一些修改,算是伪原创吧。不过目的只有一个:希望与大家交流分享。实在是没有必要再找其他的例子说明这些重构规则,因为《重构:改善既有的代码的设计》这本书真的是太经典了。
1.需要重构的代码如下所示。下方代码中的MyCustomer类中有两个常量属性,并且该类提供了一个构造器。该类还提供了一个输出方法,就是第一该类中的属性进行打印说明,其实该类中没有什么功能。
在写好需要重构的类后,我们要为该类写一个测试用例。这便于在我们重构时对重构的正确性进行验证,因为每次重构后都要去执行该测试用例,以保证我们重构是正确的。下方截图就是为上方示例写的测试用例以及该测试用例的打印结果。当然重构后我们也需要调用该测试用例,并观察打印结果是否与之前的一致。当然如果你不想自己观察,你可以为上面的类添加相应的单元测试,这也是在常规项目中经常使用的。至于如果添加测试用例,我们会在后面的博客中进行详细介绍。下方就是上述类的测试用例和输出结果:
2.接下来我们对上面类中的printOwning函数进行分析。上述类可以正常工作,这是肯定的。但是printOwning()函数写的不够好。因为它干了太多的事情,也就是说该函数包括了其他子模块,需要对其进行拆分。由上面截图中的红框可以看出,每个红框就代表着一个独立的功能模块,就说明这一块代码可以被拆分成独立的函数。在拆分子函数时,我们要为该函数起一个与改代码块功能相符合的名字。也就是说当你看到该函数名字时,你就应该知道该函数是干嘛的。
下方代码段就是我们重构后的类。说白的,就是对函数中可以进行独立的模块进行提取,并为提取的新的函数命一个合适的名称。经过重构后printOwing()函数就只有两行代码,这样看其中调用的函数名也是很容易理解其作用的。下方拆分出来的三个函数也是一个独立的模块,因为函数短小,所以易于理解,同时也易于重用。经过Extract Method,当然好处是多多的。经过重构后的代码,我在调用上述的测试用例,输出结果和原代码是一直的,如果不一致的话,那么说明你的重构有问题呢,需要你进行Debug。
二. Inline Method ---- 内联函数:将微不足道的小函数进行整合
看过《周易》的小伙伴应该都知道,《周易》所表达的思想有一点就是“物极必反”。《周易》中的六十四卦中的每一卦的“上九”(第六位的阳爻)或者“上六”(第六位的阴爻)都是物极必反的表现。其实《周易》其实就是计算机科学中二进制的表象,因为太极生两仪(2进制中的2),两仪生四象(2的平方为4),四象生八卦(4 x 2 = 8),八卦有演变出六十四卦。六十四卦的就是2进制中的0-1排列。九五至尊,九六就物极必反了。wo kao, 扯远了,言归正传,当然这提到《周易》不是说利用周易如何去算卦,如何去预测,本宝宝不信这东西。不过《周易》中的哲学还是很有必要学习一下的。有所取,有所不取。
回到本博客的主题,Inline Method其实是和Extract Method相对的。当你在重构或者平时编程时,对模块进行了过度的封装,也就是使用Extract Method有点过头了,把过于简单的东西进行了封装,比如一个简单的布尔表达式,而且该表达式只被用过一次。此时就是过度的使用Extract Method的表现了。物极必反,所以我们需要使用Inline Method进行中和,将过度封装的函数在放回去,或者将那些没有必要封装的函数放回去。也就是Extract Method相反的做法。
至于Inline Method规则的示例呢,在此就不做过多的赘述了,因为只需要你将Extract Method的示例进行翻转即可。
三.Replace Temp with Query----以查询取代临时变量: 将一些临时变量使用函数替代
1.Replace Temp with Query说白了就是将那些有着复杂表达式赋值并且多次使用的临时变量使用查询函数取代,也就是说该临时变量的值是通过函数的返回值来获取的。这样一来在实现类似功能的函数时,这些复杂的临时变量就可以进行复用,从而减少代码的重复率。下方就是Replace Temp with Query规则的一个特定Demo,接下来我们要对getPrice()函数使用Replace Temp with Query规则进行重构。
对上面的小的demo创建对应的测试用例是少不了的,因为我们要根据测试用例还测试我重构后的代码是否一致,下方截图就是该代码的测试用例以及输出结果,具体如下所示。
2.接下来就是对Procut类中的getPrice()函数进行分析并重构了。在getPrice()函数中的第一个红框中有一个basePrice临时常量,该常量有一个较为复杂的赋值表达式,我们可以对其使用Replace Temp with Query进行重构,可就是创建一个函数来返回该表达式的值。第二个红框中的discountFactor临时变量被多次使用,我们可以对其通过Replace Temp with Query规则进行重构,具体重构后的代码如下所示。
由重构后的代码容易看出,上面我们提到的临时常量或者变量都不存在了,取而代之的是两个查询方法,对应的查询方法返回的就是之前消除的临时变量或常量的值。
四、Inline Temp ---内联临时变量:与上面的Replace Temp with Query相反
当临时变量只被一个简单的表达式赋值,而该临时变量妨碍了其他重构手法。此时我们就不应该使用Replace Temp with Query。之所以有时我们会使用到Inline Temp规则,是因为Replace Temp with Query规则使用过度造成的情况,还是物极必反,使用Replace Temp with Query过度时,就需要使用Inline Temp进行修正,当然Inline Temp的示例与Replace Temp with Query正好相反,在此就不做过多的赘述了。
五、Introduce Explaining Variable---引入解释性变量:将复杂的表达式拆分成多个变量
当一个函数中有一个比较复杂的表达式时,我们可以将表达式根据功能拆分成不同的变量。拆分后的表达式要比之前未拆分的表达式的可读性更高。将表达式拆分成相应的临时变量,也就是Introduce Explaining Variable,如果临时变量被多次使用的话,我们还可以尝试着使用Replace Temp with Query规则去除临时变量,也就是将临时变量换成查询函数。
1.在下方Product类中的getPrice()方法中返回了一个比较长的表达式,第一眼看这个函数感觉会非常的不舒服。因为它返回的表达式太长了,而且可读性不太好。在这种情况下就很有必要将该表达式进行拆分。
2.接下来就可以使用Introduce Explaining Variable规则,引入解释性变量。顾名思义,我们引入的变量是为了解释该表达式中的一部分的功能的,目的在于让该表达式具有更好的可读性。使用Introduce Explaining Variable规则,就相当于为该表达式添加上相应的注释。下方截图就是使用 Introduce Explaining Variable规则进行重构后的结果。
3.引入临时变量是为了更好的可读性,如果临时变量所代表的表达式多次使用,我们就可以对上述函数在此使用Replace Temp with Query规则进行重构。也就是去除经常使用而且表达式比较复杂的临时变量,下方代码段是对上述函数进行Replace Temp with Query重构,去掉临时变量,再次重构后的结果如下所示。
六、Split Temporary Variable-----分解临时变量:一心不可二用
什么叫分解临时变量的,具体说来就是在一个函数中一个临时变量不能做两种事情,也就是一个临时变量不能赋上不同意义的值。如果你这么做了,那么对不起,请对该重复使用的临时变量进行分解,也就是说你需要创建一个新的临时变量来接收第二次分配给第一个临时变量的值,并为第二个临时变量命一个确切的名字。
下方第一个函数是重构前的,可以看出temp被重复的赋值了两次的值,如果这两个值关系不大,而且temp不足以对两个值的意思进行说明。那么就说明该段代码就应该被重构了。当然,重构的做法也是非常简单的,只需要术业有专攻即可,各司其职,并且为每个临时变量命一个合适的名字即可。具体做法如下所示。
七、Remove Assignments to Parameters----移除对参数的赋值
“移除对参数的赋值”是什么意思呢?顾名思义,就是在函数中不要对函数参数进行赋值。也就是说你在函数的作用域中不要对函数的参数进行赋值(当然,输入输出参数除外),当直接对函数的参数进行修改时,对不起,此时你应该对此重构。因为这样会是参数的原始值丢失,我们需要引入临时变量,然后对这个临时变量进行操作。
1.下方这个discount()函数就做的不好,因为在discount()函数中直接对非inout参数inputVal进行了修改并且返回了,我们不建议这样做。遇到这种情况,我们需要使用Remove Assignments to Parameters规则对下方的函数进行重构。
2.当然重构的手法也特别简单,就是需要将上面的inputVal使用函数的临时变量进行替代即可,下方就是重构后的函数。
八.Replace Method with Method Object----以函数对象取代函数
当一个特别长的函数,而且函数中含有比较复杂的临时变量,使用上述那些方法不好进行重构时,我们就要考虑将该函数封装成一个类了。这个对应的类的对象就是函数对象。我们可以将该场函数中的参数以及临时变量转变成类的属性,函数要做的事情作为类的方法。将函数转变成函数类后,我们就可以使用上述的某些方法对新的类中的函数进行重构了。具体做法请看下方示例。
1.下方示例中的discount函数有过多的参数(当然,现实项目工程中参数比这个还要多),并函数中含有多个临时变量,假设函数功能比较复杂,而且比较长。下方示例对该函数使用上述那些规则进行重构会比较复杂,此时我们就可以将该函数抽象成一个类。
2.重构的第一步就是将上述discount()函数抽象成Discount类。在Discount类中有六个属性,这六个属性分别对应着discount()函数的不同参数。除了添加参数属性外,我们在函数类提取时还添加了一个Account的委托代理对象。该委托代理对象是为了在Discount类中访问Account类中依赖的数据,下方是第一次重构后的代码。
3.接着,我们就可以在新的Discount类中的compute()方法中使用我们上述介绍的规则进行重构。对compute()方法进行分析,我们发现importandValue等属性是可以通过Replace Temp with Qurey 规则进行消除的。所为我们可以再次对上述方法进行重构,重构后的具体代码如下:
代码重构(二):类重构规则
在上篇博客《代码重构(一):函数重构规则(Swift版)》中,详细的介绍了函数的重构规则,其中主要包括:Extract Method, Inline Method, Inline Temp, Replace Temp with Query, Introduce Explaining Variable, Split Temporary Variable, Remove Assignments to Parameters, Replace Method with Method Object等。关于上述这些函数重构的规则更为详细的信息请参考上一篇博客,在此就不做过多的赘述了。
今天这篇博客主要介绍一下类的重构。在我们写代码时,有些类是不规范的,需要重构。在对类进行重构时,也是有一些章法可寻的,本篇博客就结合着相关示例,对类的重构进行相关的介绍。当然在本篇博客中使用的实例,还是延续上一篇文章的风格,仍然采用Swift语言进行编写。当然,还是那句话,重构的思想和手法与设计模式类似,都与具体语言实现无关。触类旁通,关键还是思想和手法。为了精简博文的篇幅,相关的测试用例就不往上粘贴了。当然,在你实现时,测试用例是必不可少的,因为测试用例可以在你重构时及时发现因为重构而产生的错误。言归正传,进入今天博客的主题。
一、Move Method----方法迁移
关于Move Method,首先谈论一下为什么要进行方法的迁移。原因很简单,就是当类中的方法不适合放在当前类中时,就应该为该方法寻找合适下家。那么怎样才可以称作是当前方法不适合在当前类中呢?一个类中的函数与另一个类有很多的交互,函数非常依赖于某个类。如果一个类有太多行为,或者与另一个类有太多合作而形成高度耦合。此时就应该将该方法搬移到其高度依赖的类中。
在给方法搬家时需要做的就是在方法的新家中创建一个方法,实现要搬移的功能,如果新创建的函数需要旧类中的数据,那么就创建一个委托对象来解决这个问题。说白了就是在另一个类中创建一个相同的功能的新函数,将旧函数变成一个单纯的委托函数,或者将旧函数完全移除。搬移后,我们可以再使用函数的重构规则对新组的函数进行重构。下方就通过一个实例来直观的感受一下Move Method。
1.代码实例
在下方截图中有两个类,一个Book类,另一个是BookCustomer类。在Book类中有两个属性,一个是bookCode:表示书的种类(NEW_BOOK,OLD_BOOK, CHIDREN_BOOK), 另一个属性就是书名bookName。在BookCustomer中有3个字段,name表示用户的名称,isVip表示用户是否是会员,books表示该用户所购买的书的集合。BookCustomer类中的charge()方法用来根据books数组来计算图书的总价格,并返回总价格。如果是VIP, 就在总价格的基础上打7折,普通用户打8折。下方截图就是其具体实现。
2.使用Move Method进行重构
首先我们对上述两个类进行分析,观察需要重构的地方。首先第一眼看代码时,较长的charge()函数会让我们看起来些微的不舒服,因为它太长了。再仔细分析,其中的Switch语句中的业务逻辑用的全是Book类的东西,和当前BookCustomer类没用什么关联。但是这个Switch语句是当前charge()函数的核心,也就是BookCustomer严重依赖Book类的地方。以此分析下去,我们就清楚的指定,该Switch语句块放错了地方,它应该放在Book类中。所以我们应该将这块代码进行搬移。
重构方法就是在Book类中创建一个charge()函数,将Switch语句块放入新的charge()函数中。然后在原来的charge()函数使用Switch语句时调用新的charge()方法。下方代码段是使用Move Method重构后的结果。
3.使用函数重构
在使用Move Method重构后,我们看出在BookCustomer类中的charge()函数是可以使用Extract Method和Replace Temp With Qurey进行重构的。关于这两个函数重构的规则的具体细节请参见《代码重构(一):函数重构规则(Swift版)》中的介绍。下方截图是对BookCustomer类中的charge()函数进行重构后的结果,如下所示:
二、Move Field----搬移字段
上一部分是搬移方法,Move Field(搬移字段)与Move Method适用场景类似。当在一个类中的某一个字段,被另一个类的对象频繁使用时,我们就应该考虑将这个字段的位置进行更改了。Move Field与Move Method的思想和做法差不多,再次对其的示例就省略了。举一反三,你可以类比着Move Method来使用Move Field规则。具体实现方式在此就不做过多的赘述了。
三、Extract Class----提炼类
Extract Class和Extract Method类似,Extract Method提取的是方法,而Extract Class提取的是类。一个类如果过于复杂,做了好多的事情,违背了“单一职责”的原则,所以需要将其可以独立的模块进行拆分,当然有可能由一个类拆分出多个类。当然,对类的细化也是为了减少代码的重复性,以及提高代码的复用性,便于代码的维护。下方将会通过一个实例,对类进行提炼。
1.重构前的代码
下方是我们将要进行重构的代码段。在Person类中有三个字段,常量name表示该Employee的名字,officeAreaCode表示Employee所在办公部门的区域代码。然后就是Employee类的构造函数了。Employee类比较简单。
2.使用Extract Class对Employee重构
接下来要做的就是使用Extract Class对Employee进行重构。因为上述Employee类设计的不好,因为Employee类可以再分。显然可以将区域号和电话号提取成一个TelePhoneNubmer类,在Employee中调用TelePhoneNubmer类。这样一来TelePhoneNubmer类就可以重复利用了,而且层次结构更为清晰。下方代码段就是对上述代码进行重构后的结果。具体如下所示:
四、Inline Class----类的内联化
又到了“物极必反”的时候了。Extract Method与Inline Method职责相反,Extract Class当然也就职责相反的原则。那就是接下来要介绍的类的内联化:Inline Class。如果过度使用Extract Class原则的话,会使得某些类过于简单并且调用该简单的类的地方极少。也就是说一个类根本不能称为一个类,所以我们可以通过Inline Class将过度抽象出来的类放到其他类中。
关于Inline Class的示例在此就不做过多的赘述了,因为与Extract Class原则相反,将第三部分中的示例倒着过一遍即为类的内联化的工作方式。
五、Hide Delegate----隐藏“委托关系”
隐藏类之间的“委托关系”这一原则用起来是非常不错的,它可以简化类调用委托者的方式。简单的说就是讲委托调用的链,封装成相应的方法,使其隐藏掉具体的调用细节,从而简化了调用方式。下方会根据具体事例和测试用例来介绍一下Hide Delegate。
1.重构前的案例
在下方代码片段中有两个类,这两个类互为依赖关系。Department中有People,该People对应的就是经理人。还有一个字段就是chargeCode,对应的是部门代码。而People类中有name--名字字段,department--所属部门字段。在People对象中可以委托department对象来获取经理的名字。
获取People对象所在部门经理的名字的测试用例如下所示。在下方测试用例中创建了一个经理和一个员工,并为员工和经理绑定关系。zeluLi.department.manager.name就是委托department对象来调用经理的名字,这样调用未免太长,所以有必要使用Hide Delegate原则对其进行优化。
2.使用Hide Delegate进行重构
使用Hide Delegate进行重构的方式是比较简单的,就是在People中封装一个方法,在方法中返回经理的对象即可,这样就隐藏掉了委托关系。具体实现方式如下截图所示:
添加上上面的函数后的调用方式如下:
Remove Middle Man(移除中间人)原则与Hide Delegate相反,就是没有必要将委托人进行隐藏,所以就使用Remove Middle Man原则将上面我们封装的获取委托人的方法进行移除。关于Remove Middle Man的范例就不做过多的赘述了。
六、Introduce Foreign Method----引入外加函数
这一点在开发中用的还是比较多的,有时候你在不想或者不能修改原类的情况下想为该类添加新的方法。在这种情况下就会使用到Introduce Foreign Method。在Swift语言中,使用Introduce Foreign Method原则特别简单,也就是在不改变类的情况下对类进行扩展也是特别简单的。因为Swift语言以及OC中有延展的功能,所以非常对此非常好实现的。下方的代码段就是对MyTest类使用extension为其扩展一个method2方法,具体如下所示。
代码重构(三):数据重构规则
在《代码重构(一):函数重构规则(Swift版)》和《代码重构(二):类重构规则(Swift版)》中详细的介绍了函数与类的重构规则。本篇博客延续之前博客的风格,分享一下在Swift语言中是如何对数据进行重构的。对数据重构是很有必要的,因为我们的程序主要是对数据进行处理。如果你的业务逻辑非常复杂,那么对数据进行合理的处理是很有必要的。对数据的组织形式以及操作进行重构,提高了代码的可维护性以及可扩展性。
与函数重构与类重构类似,对数据结构的重构也是有一定的规则的。通过这些规则可以使你更好的组织数据,让你的应用程序更为健壮。在本篇博客中将会结合着Swift代码实现的小实例来分析一下数据重构的规则,并讨论一下何时使用那些重构规则进行数据重构。还是那句话“物极必反”呢,如果不恰当的使用重构规则,或者过度的使用重构规则不但起不到重构的作用,有时还会起到反作用。废话少说,进入今天数据重构的主题。
一. Self Encapsulate Field (自封装字段)
"自封装字段"理解起来比较简单,一句话概括:虽然字段对外是也隐藏的,但是还是有必要为其添加getter方法,在类的内部使用getter方法来代替self.field,该方式称为自封装字段,自己封装的字段,自己使用。当然该重构规则不是必须执行的,因为如果你直接使用self来访问类的属性如果不妨碍你做扩展或者维护,那么也是可以的,毕竟直接访问变量更为容易阅读。各有各的好处,间接访问字段的好处是使你的程序更为模块化,可以更为灵活的管理数据。比如在获取值时,因为后期需求的变化,该获取的字段需要做一些计算,那么间接访问字段的方式就很容易解决这个问题,而直接访问字段的方式就不是很好解决了。所以间接一下还是好处多多的,不过直接访问不影响你的应用程序的话,也是无伤大雅的。
下方会通过一个实例来看一下间接访问字段的好处。下方的IntRange类中的字段就没有提供间接访问的方法,在代码中通过直接访问的形式来使用的字段。这种做法对当前的程序影响不大,但是如果提出需求了。要在high赋值后,在IntRange对类进行一个较为复杂的修改。那么对于下方代码而言,有两种解决方案,就是在构函数中进行修改,在一个就是在使用self.high的地方进行修正,当然这两种方法都不理想。最理性的方案是在相应字段的getter方法修改。
下方截图就是为InRange类中相应的字段自封装了getter和setter方法,并在使用self.字段的地方使用该自封装的方法代替(构造函数中对字段的初始化除外,因为设置方法一般在对象创建完毕以后在调用,所以不能在创建对象时调用,当然Swift语言也不允许你在构造函数函数中调用设置方法)。下方红框中的是我们添加的自封装方法,绿框中是对自封装方法的使用,白框中是需要注意的一点,构造函数中不能使用该设置函数。
当然,只添加上上述自封装字段后,优点不明显。当然子类CappedRange继承了IntRange函数后,这种优点就被显示了出来。在子类中CappedRange的high需要与新添加的字段cap进行比较,取较大的值作为区间的上限。在这种情况下自封装字段的优点就被凸显了出来。在子类中只需要对getHigh()函数进行重新,在重写的方法中进行相应的计算即可。因为当在子类中调用inclued()方法时,在include()方法中调用的是子类的getHigh()方法。具体请看下方子类截图:
二. Replace data Value with Object(以对象取代数据值)
“以对象取代数据值”说白了就是我们常说的实体类,也就是Model类。Model的职责就将一些相关联的数据组织在一起来表示一个实体。Model类比较简单,一般只用于数据的存储,其中有一些相关联的字段,并为这些相关联的字段添加getter/和setter方法。下方是一个Person的数据模型,我们命名为PersonModel,其中有三个表示Person属性的字段name、birthday、sender。然后提供了一个构造器以及各个属性对应的getter和setter方法。具体请看下方代码所示:
三、Change Value to Reference (将值对象改变成引用对象)
在介绍“将值对象改变成引用对象”之前,我们先去了解一下值对象和引用对象的区别。先说一下值对象,比如两个相等的数值,存入了两个值对象中,这两个值对象在内存中分别占有两块不同的区域,所以改变其中一个值不会引起另一个值得变化。而引用对象正好相反,一个内存区域被多个引用指针所引用,这些引用指针即为引用对象,因为多个指针指向同一块内存地址,所以无论你改变哪一个指针中的值,其他引用对象的值也会跟着变化。
基于值对象和引用对象的特点,我们有时候根据程序的上下文和需求需要将一些值类型改变成引用类型。因为有时候需要一些类的一些对象在应用程序中唯一。这和单例模式又有一些区别,单例就是一个类只能生成一个对象,而“将值对象改变成引用对象”面临的就是类可以创建多个对象,但是这多个对象在程序中是唯一的,并且在某一个引用点修改对象中的属性时,其他引用点的对象值也会随之改变。下方就通过一个订单和用户的关系来观察一下这个规则。
1. 值引用的实例
(1) 首先我们需要创建一个消费者也就是Customer类。Customer类比较简单,其实就是一个数据实体类。其中有name和idCard属性并对应着getter/setter方法,具体代码如下所示:
(2)、紧接着我们需要创建一个订单类,在订单创建时我们需要为该订单关联一个Customer(当然这为了简化实例,我们省略了Order中的其他字段)。该Order类的代码也是比较简单的在此就不做过的的赘述了。不过有一点需要注意的是为了测试,我们将customer设计成值类型,也就是每个Order中的customer都会占用不同的内存空间,这也就是值类型的特点之一。
(3).创建完Order与Customer类后,紧接着我们要创建测试用例了。并通过测试用例来发现问题,并在重构时对该问题进行解决。在测试用例中我们创建了三个订单,为每个订单关联一个Customer。从测试用例中可以看出,关联的消费者数据为同一个人,但是这一个人在内存中占用了不同的存储空间,如果一个订单中的用户信息进行了更改,那么其他订单中的用户信息是不会更新的。如果创建完用户后,信息不可更改,虽然浪费点存储空间,但是使用值类型是没用问题的。一旦某个订单修改了用户名称,那么就会出现数据不同步的问题。
2.将Order中Customer改为引用类型(重新设计Order类)
因为在Swift语言中类本身就是引用类型,所以在设计Order时,我们值需要将其中的customer字段改成引用外部的Customer类的对象即可。这样一来多个订单可以引用同一个用户了,而且一个订单对用户信息修改后,其他订单的用户信息也会随之改变。要实现这一点需要对Order的构造函数和customer的设置函数进行修改,将在Order内部创建Customer对象的方式改变成将外部Customer对象的引用赋值给Order中的custom对象。说白了,修改后的Order中的customer对象就是外部对象的一个引用。这种方法可以将值对象改变成引用对象
上面这种做法可以将值对象改变成引用对象,但是代价就是改变Order创建的方式。上面代码修改完了,那么我们的测试用例也就作废了,因为Order的创建方式进行了修改,需要外部传入一个Customer对象,下方截图就是我们修改后的测试用例。(如果你是在你的工程中这么去将值对象修改引用对象的,不建议这么做,下面会给出比较好的解决方案)。
3.从根本上进行重构
上面代码的修改不能称为代码的重构,因为其改变的是不仅仅是模块内部的结构,而且修改了模块的调用方式。也就是说里外都被修改了,这与我们重构所提倡的“改变模块内部结构,而不改变对外调用方式”所相悖。所以在代码重构时不要这么做了,因为上面的这种做法的成本会很高,并且出现BUG的几率也会提高。因为每个使用订单的地方都会创建一个Customer的类来支持订单的创建,那么问题来了,如果同一用户在不同地方创建订单怎么办?所以上面的做法还是有问题的,终归是治标不治本。所以我们要从根本上来解决这个问题。因为该问题是因为Customer数据不同步引起的,所以我们还得从Customer来下手。
该部分的重构,在第一部分的基础上做起。我们本次重构的目标就是“不改变对外调用方式,但能保持每个用户是唯一的”。好接下来就开始我们真正的重构工作。在本次重构中,依照重构的规则,我们不会去修改我们的测试用例,这一点很重要。
(1)从根本解决问题,首先我们对Customer进行重构。在Customer中添加了一个静态的私有变量customers, 该静态私有变量是字典类型。其中存储的就是每次创建的消费者信息。在字典中每个消费者的key为消费者独一无二的身份证信息(idCard)。在添加完上述变量后,我们需要为创建一个工厂方法createCustomer() 在工厂方法中,如果当前传入的用户信息未被存入到字典中,我们就对其进行创建存入字典,并返回该用户信息。如果传入的用户已经被创建过,那么就从字典中直接取出用户对象并返回。具体做法如下所示。
(2)、对Customer类修改完毕后,我们需要在Order中通过Customer的工厂方法来获取Customer类的实例,这样就能保证Order中的customer对象也是引用对象了。不过此时的引用对象是从Customer中获取的,而不是外部传过来的。下方是Order类中对工厂方法的调用,这样做的好处就是,我们只对模块的内部进行了修改,而测试用例无需修改。
(3)、对此次重进行测试,我们任然使用第一部分使用的测试用例。也就是说该模块对外的接口是没有变化的,下方就是对重构后的代码的测试结果。由结果可以看出,在不同订单中的用户,只要是信息一致,那么其内存地址是一致的。也就是经过重构,我们将原来的值对象改成了引用对象。
四、Change Reference to Value(将引用对象改为值对象)
将引用对象改为值对象,该重构规则正好与上面相反。在一些情况下使用值对象更为简单,更易管理,但前提是该值对象很小并且不会被改变。在这种情况下你就没有必要使用引用对象了。从上面的示例来看,使用引用对象实现起来还是较为复杂的。还是那句话,如果你的对象非常小,而且在创建后其中的数据不会被改变,如果需要改变就必须在创建一个新的对象来替换原来的对象。在这种情况下使用值对象是完全可以的。在此就不做过多的赘述了。
不过在使用值对象时,你最好为值对象提供一个重载的相等运算符用来比较值对象中的值。也就是说只要是值对象中的每个属性的值都相同,那么这两个值对象就相等。至于如何对“==” 运算符进行重载就不做过多的赘述了,因为该知识点不是本篇博客的重点。
五、Replace Array or Dictionary with Object(以对象取代数组或字典)
这一点呢和本篇博客的第二部分其实是一个。就是当你使用数组或者字典来组织数据,这些数据组合起来代表一定的意义,这是最好将其定义成一个实体类。还是那句话,定义成实体类后,数据更易管理, 便于后期需求的迭代。下方代码段就是讲相应的字典和数组封装成一个实体类,因为确实比较简单,在此就不做过多的赘述了。具体请参加下方代码段。
六、Duplicate Observed Data(复制“被监测数据”)
这一部分是比较重要的部分,也是在做UI开发时经常遇到的部分。用大白话将就是你的业务逻辑与GUI柔和在了一起,因为UI作为数据的入口,所以在写程序时,我们就很容易将数据处理的方式与UI写在一起。这样做是非常不好的,不利于代码的维护,也不利于代码的可读性。随着需求不断的迭代,版本不断的更新,UI与业务逻辑融合的代码会变得非常难于维护。所以我们还是有必要将于UI无关的代码从UI中进行分离,关于如何进行分层宏观的做法请参加之前发布的博客《iOS开发之浅谈MVVM的架构设计与团队协作》。
今天博客中的该部分是分层的微观的东西,也就是具体如何将业务逻辑从GUI中进行剥离。所以在接下来的实例中是和UI实现有关的,会根据一个比较简单的Demo来一步步的将UI中的业务逻辑进行分离。进入该部分的主题。复制“被监测数据”简单的说,就是将UI提供的数据复制一份到我们的业务逻辑层,然后与UI相应的数据进行关联,UI数据变化,被复制的业务逻辑中的数据也会随之变化。这一点也就是所谓的"响应式编程"吧,关于响应式编程,iOS开发中会经常用到ReactiveCocoa这个框架,关于ReactiveCocoa的内容,请参见之前的博客《iOS开发之ReactiveCocoa下的MVVM》。今天的示例中,使用了一个比较简单的方式来同步这些数据,使用了"事件监听机制"。下方就创建一个比较简单的Demo。
1.创建示例
要创建的示例比较简单,在UI方面,只有三个输入框用来接收加数与被加数,以及用来显示两数之和。然后使用两个UILabel来显示+号与=号。我们要实现的功能就是改变其中一个加数与被加数时,自动计算两个数的和并显示。
要实现上述功能的代码也是比较简单的,总共没有几行,下方这个类就是实现该功能的全部代码。代码的核心功能就是“获取加数与被加数的和,然后在加数与被加数的值有一个改变时,就会计算两者之和,并将和赋值给最后一个输入框进行显示”。具体代码如下所示。
1 class AddViewControllerBack: UIViewController { 2 3 //三个输入框对应的字段 4 @IBOutlet var firstNumberTextField: UITextField! 5 @IBOutlet var secondNumberTextField: UITextField! 6 @IBOutlet var resultTextField: UITextField! 7 8 override func viewDidLoad() { 9 super.viewDidLoad() 10 } 11 12 //获取第一个输入框的值 13 func getFirstNumber() -> String { 14 return firstNumberTextField.text! 15 } 16 17 //获取第二个输入框的值 18 func getSecondNumber() -> String { 19 return secondNumberTextField.text! 20 } 21 22 //加数与被加数中的值改变时会调用的方法 23 @IBAction func textFieldChange(sender: AnyObject) { 24 self.resultTextField.text = calculate(getFirstNumber(), second: getSecondNumber()) 25 } 26 27 28 //计算两个数的值 29 func calculate(first: String, second: String) -> String { 30 return String(stringToInt(first) + stringToInt(second)) 31 } 32 33 //将字符串安全的转变成整数的函数 34 func stringToInt(str: String) -> Int { 35 guard let result = Int(str) else { 36 return 0 37 } 38 return result 39 } 40 }
2.对上述代码进行分析并重构
因为代码比较简单,所以很容易进行分析。在上述UI代码中,我们很清楚的看到后两个函数,也就是calculate()与stringToInt()函数是数据处理的部分,只依赖于数据,与UI关系不是很大,所以我们可以使用复制“被监测数据”规则将该段业务逻辑代码进行提取重构。重构后UI以及UI对外的工作方式不变。
下方的Calculate类就是我们提取的数据业务类,负责处理数据。在该类中我们创建了三个属性来与UI中的输入框进行对应,这也就是所说的复制“被监测的数据”。因为和也就是resultNumber是由firstNumber和SecondNumber计算而来的,所以我们就把resultNumber定义成了计算属性,而firstNumber和secondNumber为存储属性。并为存储属性提供setter方法。在Calculate类的构造函数中,我们为两个值指定了初始化数据也就是“0”。最下方的那两个函数就是我们从UI中直接拷贝过来的数据,一点没有修改,也是可以工作的,因为这部分代码只依赖于数据,而不依赖于UI。
创建为相应的业务逻辑处理类并提取完业务逻辑后,我们需要将业务逻辑中的数据,也就是复制过来的数据与UI中的数据提供者进行绑定,并返回计算结果。下方红框中就是我们要修改的部分,在UI中我们删除掉处理业务数据的代码,然后创建也给Calculate对象,并在相应的事件监听的方法中更新Calculate对象中的数据。如下所示
七、Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
要介绍本部分呢,我想引用本篇博文中第(三)部分是实例。因为在第三部分的实例中Customer与Order的关系是单向关联的,也就是说Order引用了Customer, 而Customer没有引用Order。换句话说,我们知道这个订单是谁的,但你不知道只通过用户你是无法知道他有多少订单的。为了只通过用户我们就能知道该用户有多少订单,那么我们需要使用到“将单向关联改为双向关联”这条规则。
1. 在Customer类中添加上指向Order类的链
因为Customer没有指向Order类的链,所以我们不能获取到该用户有多少订单,现在我们就要添加上这条链。将单向关联改为双向关联,具体做法是在Customer中添加一个数组,该数组中存储的就是该用户所拥有的订单。这个数组就是我们添加的链。数组如下:
1 //添加与Order关联的链,一个用户有多个订单 2 private var orders:Array<Order> = []
在Customer中值只添加数组也是不行的呢,根据之前提到的重构规则,我们要为数组封装相应的操作方法的,下方就是我们要在Customer中添加的操作数组的方法。具体代码如下所示:
1 //====================添加================== 2 func addOrder(order: Order) { 3 self.orders.append(order) 4 } 5 6 func getOrders() -> Array<Order> { 7 return self.orders 8 }
在Order类关联Customer时,建立Customer到Order的关联。也就是将当前订单添加进该用户对应的订单数组中,具体做法如下:
与之对应的规则是Change Bidirectional Association to Unidirectional(将双向关联改为单向关联),就是根据特定需求删去一个链。就是说,原来需要双向链,可如今由于需求变更单向关联即可,那么你就应该将双向关联改为单向关联。
八、Replace Magic Number with Synbolic Constant(以字面常量取代魔法数)
这一点说白了就是不要在你的应用程序中直接出现字数值。这一点很好理解,在使用字面数值时,我们要使用定义好的常量来定义。因为这样更易于维护,如果同一个字面数值写的到处都是,维护起来及其困难。当使用字面常量时维护起来就容易许多。该规则比较容易理解,在此不做过多的赘述。看下方实例即可。对于下方的实例而言,如果在版本迭代中所需的PI的精度有所改变,那么对于替换后的程序而言,我们只需修改这个常量的值即可。
1 func test(height: Double) -> Double { 2 return 3.141592654 * height 3 } 4 5 //替换 6 let PI = 3.141592654 7 func test1(height: Double) -> Double { 8 return PI * height 9 }
九、Encapsulate Field(封装字段)
当你的类中有对外开放字段时,最好将其进行封装,不要直接使用对象来访问该字段,该优缺点与上述的“自封装字段”的优缺点类似。因为直接访问类的字段,会降低程序的模块化,不利于程序的扩充和功能的添加。再者封装是面向对象的特征之一,所以我们需要将字段变成私有的,然后对外提供相应的setter和getter方法。具体做法如下所示。
1 //重构前 2 class Person { 3 var name: String = "" 4 5 init(name: String) { 6 self.name = name 7 } 8 } 9 10 //重构后 11 class Person { 12 private var name: String = "" 13 14 init(name: String) { 15 self.name = name 16 } 17 18 func getName() -> String { 19 return name 20 } 21 22 func setName(name: String) { 23 self.name = "China:" + name 24 } 25 }
十、Encapsulate Collection(封装集合)
“封装集合”这一重构规则应该来说并不难理解。当你的类中有集合时,为了对该集合进行封装,你需要为集合创建相应的操作方法,例如增删改查等等。下方就通过一个不封装集合的实例,看一下缺点。然后将其重构。关于“封装集合”具体的细节参见下方实例。
1.未封装集合的实例
下方我们先创建一个图书馆图书类,为了简化示例,该图书类只有一个书名。下方代码段就是这个图书类,如下所示:
class LibraryBook { private var name: String init(name: String) { self.name = name } func getName() -> String { return self.name } }
紧接着要创建一个借书者,借书者中有两个字段,一个是借书者的名字,另一个是所借书籍的数组。在Lender中我们没有为lendBooks数组封装相应的方法,只为其提供了getter/setter方法,具体代码如下所示。
1 class Lender { 2 private var name: String 3 private var lendBooks: Array<LibraryBook> = [] 4 5 init(name: String) { 6 self.name = name 7 } 8 9 func getName() -> String { 10 return self.name 11 } 12 13 func setLendBooks(books: Array<LibraryBook>) { 14 self.lendBooks = books 15 } 16 17 func getLendBooks() -> Array<LibraryBook> { 18 return self.lendBooks 19 } 20 }
紧接着我们要创建一个测试用例,观察这两个类的使用方式。由下面程序的注释可知,首先我们需要创建一个books的数组,该数组就像一个篮子似的,它可以存储我们要借的书籍。让后将创建的书籍添加到该数组中,最后将books赋值给借书人中的lendBooks。如果要对书籍进行修改,那么只有先获取借书人的lendBooks, 然后进行修改,最后再将修改后的值赋值回去。
1 //先创建一个书籍数组 2 var books: Array<LibraryBook> = [] 3 //添加要借的书籍 4 books.append(LibraryBook(name: "《雪碧加盐》")) 5 books.append(LibraryBook(name: "《格林童话》")) 6 books.append(LibraryBook(name: "《智慧意林》")) 7 8 //创建借书人 9 let lender: Lender = Lender(name: "ZeluLi") 10 lender.setLendBooks(books) 11 12 //获取所借书籍 13 var myBooks = lender.getLendBooks() 14 15 //对书籍数组修改后再赋值回去 16 myBooks.removeFirst() 17 lender.setLendBooks(myBooks)
2.为上面的Lender类添加相应的集合操作的方法
由上面的测试用例可以看出,Lender类封装的不好。因为其使用方式以及调用流程太麻烦,所以我们得重新对其进行封装。所以就会用到“Encapsulate Collection”原则。下面我们就会为Lender添加上相应的集合操作的方法。说白了,就是讲上面测试用例做的一部分工作放到Lender类中。下方是为Lender添加的对lendBooks相应的操作方法。下方代码中的Lender类与上面的Lender类中的lendBooks不同,我们使用了另一个集合类型,也就是字典,而字典的key就是书名,字典的值就是书的对象。具体代码如下所示:
经过上面这样一封装的话,使用起来就更为合理与顺手了。用大白话讲,就是好用。下方是我们重新封装后的测试用例,简单了不少,而且组织也更为合理。具体请看下方代码段:
十一、Replace Subclass with Fields(以字段取代子类)
什么叫“以字段取代子类”呢?就是当你的各个子类中唯一的差别只在“返回常量数据”的函数上。当遇到这种情况时,你就可以将这个返回的数据放到父类中,并在父类中创建相应的工厂方法,然后将子类删除即可。直接这样说也许有些抽象,接下来,我们会通过一个小的Demo来看一下这个规则具体如何应用。1.创建多个子类,并每个子类只有一个函数的返回值不同
接下来我们就要创建重构前的代码了。首先我们创建一个PersonType协议(也就是一个抽象类),该协议有两个方法,一个是isMale(),如果是子类是男性就返回true,如果子类是女性就返回false。还有一个是getCode()函数,如果子类是男性就返回“M”,如果是子类是女性就返回“F”。 这两个子类的差别就在于各个函数返回的值不同。下方是PersonType的具体代码。
1 protocol PersonType { 2 func isMale() -> Bool 3 func getCode() -> String 4 }
然后我们基于PersonType创建两个子类,一个是Male表示男性,一个是Female表示女性。具体代码如下:
1 class Male: PersonType { 2 func isMale() -> Bool { 3 return true 4 } 5 6 func getCode() -> String { 7 return SenderCode.Male.rawValue 8 } 9 } 10 11 class Female: PersonType { 12 func isMale() -> Bool { 13 return false 14 } 15 16 func getCode() -> String { 17 return SenderCode.Female.rawValue 18 } 19 }
上述代码的SenderCode是我们自定义的枚举类型,用来表示"M"与“F”,枚举的代码如下:
1 enum SenderCode: String { 2 case Male = "M" 3 case Female = "F" 4 }
2.以字段取代子类
从上面的代码容易看出,Male与Female类实现相同的接口,但接口函数在两个类中的返回值是不同的。这时候我们就可以使用“以字段取代子类”的方式来进行重构,下方截图就是重构后的代码片段。
下方代码中,将PersonType声明了一个类,在类中添加了两个字段,一个是isMale,另一个是code,这两个字段恰好是上述两个子类函数中返回的不同值。这也就是使用字段来取代子类,因为有了这两个字段,我们就可以不用去创建子类了,而是直接在PersonType中通过工厂方法根据不同的性别分别给这两个新加的字段赋上不同的值。具体做法如下。
经过上面这段代码重构后,我们就可以调用PersonType的不同的工厂方法来创建不同的性别了。测试用例如下所示:
代码重构(四):条件表达式重构规则
继续更新有关重构的博客,前三篇是关于类、函数和数据的重构的博客,内容还算比较充实吧。今天继续更新,本篇博客的主题是关于条件表达式的重构规则。有时候在实现比较复杂的业务逻辑时,各种条件各种嵌套。如果处理不好的话,代码看上去会非常的糟糕,而且业务逻辑看上去会非常混乱。今天就通过一些重构规则来对条件表达式进行重构,让业务逻辑更为清晰,代码更以维护和扩展。
今天博客中的代码示例依然是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方法。关键的是根据不同的书籍类型创建不同的书籍价格对象。这样一来,我们就把每个分支中的业务逻辑进行了分离,并使用了多态来获取价格。重构后的优点不言而喻。
代码重构(五):继承关系重构规则
陆陆续续的发表了多篇关于重构的文章了,还是那句话,重构是一个项目迭代开发中必不可少的一个阶段。其实重构伴随着你的项目的整个阶段。在前几篇关于重构的文章中我们谈到了函数的重构、类的重构、数据的重构以及条件表达式的重构,那么今天咱们就来聊聊继承关系的重构。当然还是延续前几篇博客的风格,我们在博客中的代码实例依然使用Swift语言来实现,当然还是那句话,使用什么语言无所谓,关键是看重构的场景以及重构的思想。
“重构”不仅仅可以改善你既有的代码设计,还可以改变你组织代码的思路,使你的程序在设计之初就趋于合理化,利于程序的扩充。重构往往伴随着设计模式的使用,在重构系列的博客结束后,我想系统的给大家分享一下关于设计模式的东西。当然是结合着各种实例。所谓一名Coder,重构和设计模式是必须涉猎的部分,因为这两者可以让你写出更漂亮的代码,当然要想真正的掌握设计模式以及各种重构手法,还得结合不同的实例来进行实践。理论固然重要,但是要想将理论的东西变成你自己的,还必须将理论付诸实践。废话少说,进入今天的主题。
一.Pull Up Field (字段上移) & Pull Down Field (字段下移)
字段上移与字段下移是相对的,也是我们之前所说的“凡事都有其两面性”,我们要辩证的去看待。我们只对Pull Up Field (字段上移) 这个规则做讨论,那么关于Pull Down Field (字段下移)我们不做过多的讨论,因为这两条规则是相反的,理解一条后,把这条规则反过来就是我们要理解的另一条规则。这样说起来,还是比“举一反三”要容易的多。
下方这个实例是为了解释“字段上移”所实现的一个Demo。当然Demo看上去不仅简单而且是有些夸张的,不过说明字段上移这个规则是完全足够了的。比如我们有一个父类为MySuperClass,我们有一个子类SubClass1,而在SubClass1中有一个字段父类是没有的。因为后期需求迭代或者需求变更,我们需要再创建一个SubClass1的兄弟类,就是下方的SubClass2。在SubClass2中与SubClass1中存在相同的字段,那就是var a = 0。
在上述情况下,就需要使用到我们的“字段上移”的规则。也就是说将子类中相同的字段移到父类中。在该实例中就是讲var a = 0 移到父类中。重构后的代码如下所示:
而将“Pull Down Field (字段下移)”正好与上面的情况相反。也就是父类中有某些字段,但是这些字段只有在少数子类中使用到,在这种情况下我们需要将这个字段移到相应的子类中即可。除了Pull Up Field (字段上移) & Pull Down Field (字段下移) 这两个规则外,Pull Up Method (将函数上移) 和 Pull Down Method (将函数下移)这两个规则与上述情况类似。就是将上面的字段改成函数,有时候不仅字段会出现上述情况,函数也会出现上述情况,需要我们进行移动。因为使用场景类似,再次就不做过多的赘述了。
二、Extract Subclass (提炼子类)
这种情况下用的还是比较多的,当类中的某些方法只有在特定的类的实例中才会使用到,此时我们就需要提炼出一个子类,将该方法放到相应的子类中。这样一来我们的每个类的职责更为单一,这也就是我们常说的“单一职责”。
在下方示例中,CustomerBook是一个图书消费者的类。其中customeCharge()方法是普通用户计算消费金额所需的方法,而vipCharge()方法是VIP用户调用的方法,在内部vipCharge()需要调用customeCharege()方法。但是对外部而言,vipCharge()方法只有VIP用户才会用到,在这种情况下我们就需要使用“Extract Subclass (提炼子类)”规则对VIP进行提炼。
具体做法是我们需要提炼出一个子类,也就是说将VIP用户作为普通用户的子类,然后将只有VIP用户才调用的方法放到我们的VIP子类中。这样一来层次更加明确,每个类的职责更为单一。上述示例重构后的结果如下所示。
与“提炼子类”规则相对应的是“Collapse Hierarchy (折叠继承关系)”。一句话来概括:就是当你的父类与子类差别不大时,我们就可以将子类与父类进行合并。将上面的示例翻转就是“Collapse Hierarchy (折叠继承关系)”规则的示例,再次就不做过多的赘述了。
三、Form Template Method (构造模板函数)
Form Template Method (构造模板函数)这一规则还是比较实用的。先说模板,“模板”其实就是框架,没有具体的实现细节,只有固定不变的步骤,可以说模板不关心具体的细节。举个栗子🌰,像前段时间比较火的“秘密花园”,那些没有颜色的线条就是模板,如果一些人获取的是同一本秘密花园,那么说明每个人所获取的模板是相同的。但是每个人对每块的区域所图的颜色又有差异,这就是实现细节的不同。
言归正传,当两个兄弟类中的两个函数中的实现步骤大致一直,但是具体细节不同。在这种情况下,我们就可以将大体的步骤提取成模板,放到父类中,而具体细节由各自的子类来实现。具体实现请看下方的类,在Subclass1和Subclass2中的calculate()方法中的大体步骤是相同的,就是对两个值相加,然后返回这两个值的和。但是具体细节不同,可以看出两个相加值的具体计算方式不同。
在上述情况下我们就可以使用“Form Template Method (构造模板函数)”规则将相同的计算流程进行提取,也就是构造我们的模板函数。将模板函数放到两个类的父类中,然后在相应的子类中只给出实现细节即可。下方代码段是重构后的代码,父类中多出的方法就是我们提取的模板函数,而子类中只给出相应的实现细节即可。
四、以委托取代继承(Replace Inheritance with Delegation)
有时候我们为一些类创建子类后,发现子类只使用了父类的部分方法,而且没有继承或者部分继承了父类的数据。在这种情况下我们就可以将这种继承关系修改成委托的关系。具体做法就是修改这种继承关系,在原有子类中添加父类的对象字段,在子类中创建相应的方法,在方法中使用委托对象来调用原始父类中相应的方法。
下方示例是我们假想出来的,但是说明该规则是绰绰有余了。我们假设SubClass01类中只会用到SuperClass01中的display()方法,而没有继承父类中的数据。在下方示例中是继承关系,在这种情况下我们需要将其转换成委托关系。
下方是我们重构后的代码,在下方代码中我们去除了之前的继承关系。并在子类中创建了一个之前父类的代理对象,并且创建了一个相应的方法,在该新建的方法中通过代理对象来调用相应的方法。具体如下所示。
上述规则与以继承取代委托(Replace Delegation with Inheritance)原则相对于,使用情况与上述相反,再次就不做过多的赘述了。
代码重构(六):代码重构完整案例
无论做什么事情呢,都要善始善终呢。前边连续发表了5篇关于重构的博客,其中分门别类的介绍了一些重构手法。今天的这篇博客就使用一个完整的示例来总结一下之前的重构规则,也算给之前的关于重构的博客画一个句号。今天的示例借鉴于《重构,改善既有代码的设计》这本书中的第一章的示例,在其基础上做了一些修改。今天博客从头到尾就是一个完整的重构过程。首先会给出需要重构的代码,然后对其进行分析,然后对症下药,使用之前我们分享的重构规则对其进行一步步的重构。
先来聊一下该示例的使用场景(如果你有重构这本书的话,可以参加第一章中的示例,不过本博客中的示例与其有些出入)。就是一个客户去DVD出租的商店里进行消费,下方的程序是给店主用的,来根据用户所借的不同的DVD种类和数量来计算该用户消费的金额和积分。需求很简单而且也不难理解。今天博客会给出原始的代码,也是需要进行重构的代码。当然原始代码完全符合需求,并且可以正确执行。废话少说,先看示例吧。
一、需要重构的代码
在本篇博客的第一部分,我们先给出完成上述需求需要重构的代码。然后在此基础上进行分析,使用之前我们提到过的重构手法进行重构。首先我们给出了电影类的实现。在Movie类中有电影的种类(静态常量):普通电影、儿童电影、新电影,然后有两个成员变量/常量是priceCode(价格代码)、title(电影名称),最后就是我们的构造方法了。该Movie类比较简单,在此就不做过多的赘述了。
实现完Movie类接下来就是租赁类Rental,这个Rental类的职责就是负责统计某个电影租赁的时间。下方就是这个租赁类,该类也是比较简单的,其中有两个字段,一个是租了的电影,另一个就是租赁的时间了。
接下来要实现我们的消费者类了,也就是Customer类。在Customer类中有消费者的名字name和一个数组,该数组中寸的就是租赁电影的集合。其中的statement()方法就是结算该客户的结算信息的方法,并将结果进行打印。在此我们需要了解的需求是每种电影的计价方式以及积分的计算规则。
电影价格计算规则:
普通片儿--2天之内含2天,每部收费2元,超过2天的部分每天收费1.5元
新片儿--每天每部3元
儿童片--3天之内含3天,每部收费1.5元,超过3天的部分每天收费1.5元
积分计算规则:
每借一步电影积分加1,新片每部加2
statement()函数中所做的事情就是根据上面的计算规则,根据用户所租赁的电影的不同来进行金额的计算和积分的计算的。
如果你看代码不太直观的话,下面我使用了startUML简单的画了一个UML的类图来说明上述三个类中的依赖关系。具体如下所示:
在对上面代码重构之前呢,我们还必须有上述代码的测试用例。因为在每次重构之前,我们修改的是代码的内部结构,而代码模块对外的调用方式不会变的。所以我们所创建的测试用例可以帮助验证我们重构后的程序是否可以正常的工作,是否重构后还符合我们的需求。下方就是我们创建的测试用例(当然,在iOS开发中你可以使用其他的测试框架来进行单元测试,重构时,单元测试是少不了的)。在本篇博客中重构后的代码仍然使用下方的测试用例。
1 //测试用例-------------------------------------------------------------------- 2 //创建用户 3 let customer = Customer(name: "ZeluLi") 4 5 //创建电影 6 let regularMovie:Movie = Movie(title: "《老炮儿》", priceCode: Movie.REGULAR) 7 let newMovie:Movie = Movie(title: "《福尔摩斯》", priceCode: Movie.NEW_RELEASE) 8 let childrenMovie:Movie = Movie(title: "《葫芦娃》", priceCode: Movie.CHILDRENS) 9 10 //创建租赁数据 11 let rental1:Rental = Rental(movie: regularMovie, daysRented: 5) 12 let rental2:Rental = Rental(movie: newMovie, daysRented: 8) 13 let rental3:Rental = Rental(movie: childrenMovie, daysRented: 2) 14 15 customer.rentals.append(rental1) 16 customer.rentals.append(rental2) 17 customer.rentals.append(rental3) 18 19 let result = customer.statement() 20 print(result)
针对上述案例,上面测试用例的输出结果如下。在每次重构后,我们都会执行上述测试代码,然后观察结果是否与之前的相同。当然如果你的是单元测试的话,完全可以把对结果检查的工作交给单元测试中的断言来做。
二、重构1:对较statement函数进行拆分
1.对statement()函数使用“Extract Method”原则
在上面的案例中,最不能容忍的,也就是最需要重构的首先就是Customer中的statement()函数。statement()函数最大缺点就是函数里边做的东西太多,我们第一步需要做的就是对其进行拆分。也就是使用我们之前提到过的“Extract Method”(提炼函数)原则对该函数进行简化和拆分。将statement()中可以独立出来的模块进行提取。经过分析后的,我们不难发现下方红框当中的代码是一个完整的模块,一个是进行单价计算的,一个是进行积分计算的,我们可以将这两块代码进行提取并封装成一个新的方法。在封装新方法时,要给这个新的方法名一个恰当的函数名,见名知意。
下方这块代码就是我们对上面这两个红框中的代码的提取。在提取时,将依赖于statement()函数中的数据作为新函数的参数即可。封装后的方法如下,在statement函数中相应的地方调用下方的方法即可。下方就是我们封装的计算当前电影金额和计算积分的函数。这两个函数都需要传入一个Rental的对象。
//根据租赁订单,计算当前电影的金额 func amountFor(aRental: Rental) -> Double { var result:Double = 0 //单价变量 switch aRental.movie.priceCode { case Movie.REGULAR: result += 2 if aRental.daysRented > 2 { result += Double(aRental.daysRented - 2) * 1.5 } case Movie.NEW_RELEASE: result += Double(aRental.daysRented * 3) case Movie.CHILDRENS: result += 1.5 if aRental.daysRented > 3 { result += Double(aRental.daysRented - 3) * 1.5 } default: break } return result } //计算当前电影的积分 func getFrequentRenterPoints(rental: Rental) -> Int { var frequentRenterPoints: Int = 0 //用户积分 frequentRenterPoints++ if rental.movie.priceCode == Movie.NEW_RELEASE && rental.daysRented > 1{ frequentRenterPoints++ } return frequentRenterPoints }
经过上面的重构步骤,我们会运行一下测试用例或者执行一下单元测试,看是否我们的重构过程引起了新的bug。
三、重构2:将相应的方法移到相应的类中
经过上面的重构,我们从statement()函数中提取了两个方法。观察这两个重构后的方法我们不难看出,这两个封装出来的新的方法都需要一个参数,这个参数就是Rental类的对象。也就是这两个方法都依赖于Rental类,而对该函数所在的当前类不太感冒。出现这种情况的原因就是这两个函数放错了地方,因为这两个函数放在Customer类中不依赖与Customer类而依赖于Rental类,那就足以说明这两个方法应该放在Rental类中。
经过我们简单的分析后,我们就可以决定要将我们新提取的方法放到Rental类中,并且函数的参数去掉。因为函数在Rental类中,所以在函数中直接使用self即可。将计算金额的方法和计算积分的方法移到Rental类中后,我们的Rental类如下所示。在我们的Customer中的statement()方法中在计算金额和计算积分时,直接调用Rental中的方法即可。经过这一步重构后,不要忘记执行一下你的测试用例,监测一下重构的结果是否正确。
四、使用“以查询取代临时变量”再次对statement()函数进行重构
经过第二步和第三步的重构后,Customer中的statement()函数如下所示。在计算每部电影的金额和积分时,我们调用的是Rental类的对象的相应的方法。下方的方法与我们第一部分的方法相比可谓是简洁了许多,而且易于理解与维护。
不过上面的代码仍然有重构的空间,举个例子,如果我们要将结果以HTML的形式进行组织的话,我们需要将上面的代码进行复制,然后修改result变量的文本组织方式即可。但是这样的话,其中的好多临时变量也需要被复制一份,这是完全相同的,这样就容易产生重复的代码。在这种情况下,我们需要使用“Replace Temp with Query”(已查询取代临时变量)的重构手法来取出上面红框中的临时变量。
上面红框中的每个临时变量我们都会提取出一个查询方法,下方是使用“Replace Temp with Query”(已查询取代临时变量)规则重构后的statement()函数,以及提取的两个查询函数。
经过上面这些步骤的重构,我们的测试用例依然不变。在每次重构后我们都需要调用上述的测试用例来检查重构是否产生了副作用。现在我们的类间的依赖关系没怎么发生变化,只是相应类中的方法有些变化。下方是现在代码所对应的类图,因为在上述重构的过程中我们主要做的是对函数的重构,也就是对函数进行提取,然后将提取的函数放到相应的类中,从下方的简化的类图中就可以看出来了。
五. 继续将相应的函数进行移动(Move Method)
对重构后的代码进行观察与分析,我们任然发现在Rental类中的getCharge()函数中的内容与getFrequentRenterPoints()函数中的内容对Movie类的依赖度更大。因为这两个函数都只用到了Rental类中的daysRented属性,而多次用到了Movie中的内容。因此我们需要将这两个函数中的内容移到Movie类中更为合适。所以我继续讲该部分内容进行移动。
移动的方法是保留Rental中这两个函数的声明,在Movie中创建相应的函数,将函数的内容移到Movie中后,再Rental中调用Movie中的方法。下方是我们经过这次重构后我们Movie类中的内容。其中红框中的内容是我们移过来的内容,而绿框中的参数需要从外界传入。
将相应的方法体移动Movie类中后,在Rental中我们需要对其进行调用。在调用相应的方法时传入相应的参数即可。下方就是经过这次中国Rental类的代码,绿框中的代码就是对Movie中新添加的方法的调用。
经过上面的重构,我们的方法似乎是找到了归宿了。重构就是这样,一步步来,不要着急,没动一步总是要向着好的方向发展。如果你从第一部分中的代码重构到第五部分,似乎有些困难。经过上面这些间接的过程,感觉也是挺愉快的蛮。下方是经过我们这次重构的类图。
六、使用“多态”取代条件表达式
在我们之前的博客中对条件表达式进行重构时,提到了使用类的多态对条件表达式进行重构。接下来我们就要使用该规则对Movie类中的getCharge()与getFrequentRenterPoints()函数进行重构。也就是使用我们设计模式中经常使用的“状态模式”。在该部分我们不需要对Rental类和Customer类进行修改,只对Movie类修改,并且引入相应的接口和继承关系。
我们对Movie类中的getCharge()方法中的Switch-Case结构观察时,我们很容易发现,此处完全可以使用类的多态来替代(具体请参见《代码重构(四):条件表达式重构规则(Swift版)》)。具体实现方式是将不通的价格计算方式提取到我们新创建的价格类中,每种电影都有自己价格类,而这些价格类都实现同一个接口,这样一来在Movie中就可以使用多态来获取价格了。积分的计算也是一样的。下方是我们要实现结构的类图。下方红框中是在原来基础上添加的新的接口和类,将条件表达式所处理的业务逻辑放在了我们新添加的类中。这样我们就可以使用类的多态了,而且遵循了“单一职责”。
下方代码就是上面大的红框中所对应的代码实现。Price是我们定义好的协议,在协议中规定了遵循该协议的类要实现的方法。而在每个具体实现类中实现了相同的接口,但是不同的类中相同的方法做的事情不同。在不同的类中的getCharge()中要做的事情就是Switch-Case语句中所处理的数据。
添加上上面的结构以后,在么我们的Movie中就可以使用多态了,在Movie中添加了一个Price声明的对象,我们会根据不同的priceCode来给price变量分配不同的对象。而在getCharge()中只管调用price的getCharge()函数即可,具体做法如下。
今天的博客到这儿也就差不多了,其实上面的代码仍然有重构的空间,如果我们想把Switch-Case这个结构去掉的话,我们可以在上面代码的基础上创建多个工厂方法即可。在此就不过赘述了。
如果看完今天的博客的内容不够直观的话,那么请放心。本篇博客中每次重构过程的完整实例会在github上进行分享。对每次重构的代码都进行了系统的整理。今天博客中的代码整理的结果如下。