代码重构 代码重构(一):函数重构规则 窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型 代码重构(二):类重构规则 代码重构(一):函数重构规则(Swift版) 代码重构 (

代码重构(一):函数重构规则

 

重构是项目作到必定程度后必然要作的事情。代码重构,能够改善既有的代码设计,加强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,咱们在项目中所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但这些功能模块有可能在下一次需求中不在适用。或者你由于需求迭代与变动,使你原有的方法或者类变得臃肿,以及各个模块或者层次之间耦合度增长。此时,你要考虑重构了。html

 

重构,在《重构,改善既有代码的设计》这本经典的书中给出了定义,大概就是:在不改变代码对外的表现的状况下,修改代码的内部特征。说白了,就是咱们的测试用例不变,而后咱们对既有的代码的结构进行修改。重构在软件开发中是常常遇到的,也是很是重要的。在需求迭代,Debug,Code Review时,你均可以对你既有的代码进行重构。git

 

在接下来的几篇博文中,我想与你们一块去窥探一下代码重构的美丽,学习一下代码重构的一些规则。固然在每一个规则中都有小的Demo, 在本篇博客以及相关内容的博客是使用Swift语言实现的。固然,和设计模式相同,重构重要的是手法和思想,和使用什么样的语言关系不大。经典的重构书籍中是使用Java语言来实现的,若是你对PHP, Python等其余语言比较熟悉,彻底能够使用这些语言来测试一些重构手法。github

 

本篇博客的主题就是经过一些列的重构手法,对既有的须要重构的函数或者方法进行重构。而且会将每一个示例在GitHub上进行分享,感兴趣的小伙伴能够对其进行下载。有的小伙伴说了,我没有Mac,怎么对你写的Swift代码进行编译呢?这个问题好解决,你能够看我以前发表的这篇博客《窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型》。你能够将相关代码进行拷贝,在浏览器中观察结果。由于在线编译的网站是国外的,访问起来也许会有一些卡顿,不过是能够用的。好前面扯了这么多了,进入今天的主题。编程

 

1、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规则进行重构,具体重构后的代码以下所示。

由重构后的代码容易看出,上面咱们提到的临时常量或者变量都不存在了,取而代之的是两个查询方法,对应的查询方法返回的就是以前消除的临时变量或常量的值。

   

 

4、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正好相反,在此就不作过多的赘述了。

 

5、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重构,去掉临时变量,再次重构后的结果以下所示。

    

 

6、Split Temporary Variable-----分解临时变量:一心不可二用

什么叫分解临时变量的,具体说来就是在一个函数中一个临时变量不能作两种事情,也就是一个临时变量不能赋上不一样意义的值。若是你这么作了,那么对不起,请对该重复使用的临时变量进行分解,也就是说你须要建立一个新的临时变量来接收第二次分配给第一个临时变量的值,并为第二个临时变量命一个确切的名字。

下方第一个函数是重构前的,能够看出temp被重复的赋值了两次的值,若是这两个值关系不大,并且temp不足以对两个值的意思进行说明。那么就说明该段代码就应该被重构了。固然,重构的作法也是很是简单的,只须要术业有专攻便可,各司其职,而且为每一个临时变量命一个合适的名字便可。具体作法以下所示。

 

 

 7、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语言进行编写。固然,仍是那句话,重构的思想和手法与设计模式相似,都与具体语言实现无关。举一反三,关键仍是思想和手法。为了精简博文的篇幅,相关的测试用例就不往上粘贴了。固然,在你实现时,测试用例是必不可少的,由于测试用例能够在你重构时及时发现由于重构而产生的错误。言归正传,进入今天博客的主题。

 

1、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()函数进行重构后的结果,以下所示:

   

 

2、Move Field----搬移字段

上一部分是搬移方法,Move Field(搬移字段)与Move Method适用场景相似。当在一个类中的某一个字段,被另外一个类的对象频繁使用时,咱们就应该考虑将这个字段的位置进行更改了。Move Field与Move Method的思想和作法差很少,再次对其的示例就省略了。触类旁通,你能够类比着Move Method来使用Move Field规则。具体实现方式在此就不作过多的赘述了。

 

3、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类就能够重复利用了,并且层次结构更为清晰。下方代码段就是对上述代码进行重构后的结果。具体以下所示:

 

 

4、Inline Class----类的内联化

又到了“物极必反”的时候了。Extract Method与Inline Method职责相反,Extract Class固然也就任责相反的原则。那就是接下来要介绍的类的内联化:Inline Class。若是过分使用Extract Class原则的话,会使得某些类过于简单而且调用该简单的类的地方极少。也就是说一个类根本不能称为一个类,因此咱们能够经过Inline Class将过分抽象出来的类放到其余类中。

关于Inline Class的示例在此就不作过多的赘述了,由于与Extract Class原则相反,将第三部分中的示例倒着过一遍即为类的内联化的工做方式。

 

5、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的范例就不作过多的赘述了。

 

6、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方法。具体请看下方代码所示:

   

 

3、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)、对这次重进行测试,咱们任然使用第一部分使用的测试用例。也就是说该模块对外的接口是没有变化的,下方就是对重构后的代码的测试结果。由结果能够看出,在不一样订单中的用户,只要是信息一致,那么其内存地址是一致的。也就是通过重构,咱们将原来的值对象改为了引用对象。

    

 

4、Change Reference to Value(将引用对象改成值对象)

将引用对象改成值对象,该重构规则正好与上面相反。在一些状况下使用值对象更为简单,更易管理,但前提是该值对象很小而且不会被改变。在这种状况下你就没有必要使用引用对象了。从上面的示例来看,使用引用对象实现起来仍是较为复杂的。仍是那句话,若是你的对象很是小,并且在建立后其中的数据不会被改变,若是须要改变就必须在建立一个新的对象来替换原来的对象。在这种状况下使用值对象是彻底能够的。在此就不作过多的赘述了。

不过在使用值对象时,你最好为值对象提供一个重载的相等运算符用来比较值对象中的值。也就是说只要是值对象中的每一个属性的值都相同,那么这两个值对象就相等。至于如何对“==” 运算符进行重载就不作过多的赘述了,由于该知识点不是本篇博客的重点。

 

5、Replace Array or Dictionary with Object(以对象取代数组或字典)

这一点呢和本篇博客的第二部分实际上是一个。就是当你使用数组或者字典来组织数据,这些数据组合起来表明必定的意义,这是最好将其定义成一个实体类。仍是那句话,定义成实体类后,数据更易管理, 便于后期需求的迭代。下方代码段就是讲相应的字典和数组封装成一个实体类,由于确实比较简单,在此就不作过多的赘述了。具体请参加下方代码段。

    

 

6、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对象中的数据。以下所示

   

 

7、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(将双向关联改成单向关联),就是根据特定需求删去一个链。就是说,原来须要双向链,可现在因为需求变动单向关联便可,那么你就应该将双向关联改成单向关联。

 

8、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 }
复制代码

 

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 }
复制代码

 

10、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就是书名,字典的值就是书的对象。具体代码以下所示:

   

 

通过上面这样一封装的话,使用起来就更为合理与顺手了。用大白话讲,就是好用。下方是咱们从新封装后的测试用例,简单了很多,并且组织也更为合理。具体请看下方代码段:

    

 

11、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的逻辑块所作的东西很少。不过咱们能够假设一下,若是在比较复杂的状况下,这种重构手法是比较实用的。具体的你们就看重构前与重构后的区别吧。

    

 

2、Consolidate Conditional Expression(合并条件表达式)

“合并条件表达式”这条规则也是比较好理解的,由于有时候会存在这样的状况,也就是一些条件表达式后的语句体执行的代码块相同。说白了也就是不一样的条件有着一样的返回结果。固然通常在你程序设计之初不会出现此问题,由于在咱们设计程序时,若是不一样的条件返回相同的结果,咱们确定会将其合并的。不过当你在多个版本迭代,多个需求要增长,或者在别人的代码上进行需求迭代的时候,该状况是颇有可能发生的。

说这么多,也许有些抽象,那么就直接看下方须要重构的Demo了。固然,下方的Demo中,咱们为了测试,其中的条件比较简单。咱们假设每一个条件表达式是在不一样的需求迭代中或者修改bug时添加的,从而形成了下方这种状况(固然下方的状况有些夸张,这也是为了突出要合并条件的状况)。

 

在上述夸张的Demo中一眼就能看出来如何进行重构了(在平常开发迭代中,由于业务逻辑的复杂性或者屡次迭代的缘由,每每不是那么一目了然)。接下来咱们就要对不一样条件,但返回相同结果的部分进行合并。下方就是咱们合并后的结果,重构手法就是讲不一样的条件表达式使用&&或者||等布尔运算进行合并。

   

合并后,若是条件比较复杂,那么咱们就能够使用本片博客中的第一部分使用的重构规则进行再次重构。下方代码段是进行第二次重构,就是对比较复杂的表达式进行函数封装,具体以下所示。仍是那句话,Demo有些夸张,不过用来演示该重构规则也是不错的,思想就这个思想,具体在平常开发中的使用场景仍是须要进行琢磨和推敲的。

   

 

3、Consolidate Duplicate Conditional Fragments(合并重复的条件片断)

第二部分合并的是条件表达式,本部分是合并的是重复的条件片断。什么叫合并重复的条件片断呢?这种状况也是通常不会在设计程序之初所出现,可是随着时间的推移,项目不断迭代更新,或者需求变动和迭代更新等等,在项目后期维护时比较容易出现重复的条件片断。在开发中是比较忌讳重复的代码的,若是出现重复的代码,那么说明你的代码应该被重构了。   

下方代码片断中if与else中有着相同的语句,就是这个print语句。固然这个示例也是比较夸张的,可是足以说明问题。若是你在开发业务逻辑比较复杂的条件表达式时,要谨慎的检查一下有没有下方这种状况。也就是出现了重复的条件片断。这种状况在需求迭代或者变动中是及其容易出现的。固然下方只是咱们这儿列举的一个夸张的示例。

   

对于这个示例而言,咱们不难看出,去代码的重复化。将print语句移到条件以外。可是要学会触类旁通呢,重要的是重构手法和思想。在真正的项目中,若是你要提取重复的代码段通常还要结合着其余重构手法,好比将重复的部分先提取成一个独立的模块(独立的类或者方法),而后在条件中使用,最后再去重复话。这样一来,重构的思路就比较清晰了。虽然今天的示例比较简单,可是足以表达这个思路。下方是重构后的代码。若是你对下方代码看着不爽的话,彻底能够根据以前咱们介绍的重构手法“使用查询来替代临时变量”,将下方的代码继续重构,在本章博客中就不作过多介绍了。

    

 

4、Remove Control Flag(移除控制标记)

“移除控制标记”这一点仍是比较重要的,我平时在代码开发中有时候也会使用到标记变量,来标记一些事物的状态。使用标记变量最直观的感觉就是不易维护,不易理解。由于在需求变动或者迭代中,你还得维护这标记变量。若是维护一个标记变量简单的话,那么维护多个标记变量就没这么容易了。并且在你的程序中使用标记变量时,不易理解,而且会显得逻辑混乱。固然这是个人直观感觉,在写程序时,我尽可能会避免使用标记变量。

固然,下方又是一个有点夸张的例子,可是该例子能够说明问题。下方代码中咱们使用了一个flag标记变量,固然下方代码没有什么意义了。在平时开发中咱们会使用一些标记变量来标记一个或者一些数据的状态,或者一些控件的状态,再次为了简化示例,咱们就简单的引入了一个flag标记变量。下方代码不难理解,当i为20时,咱们就翻转标记变量的状态,而后if中的语句块就不被执行了。

虽然下方代码片断是我写的,可是我我的看着超级的不舒服。引入的这个flag增长了代码的逻辑复杂度,让代码变得不那么直观。我我的建议,在平时开发中尽可能的要少使用标记变量。不到万不得已,不要在你的代码中引入标记变量。若是有,尝试着去除标记变量。

   

标记变量通常是能够使用其余语句进行替换的,能够使用break、return、continue等等,这个要根据具体状况而定。总之,代码中有标记变量不是什么好的事情。下方代码段就是对上述代码去除标记变量的重构。重构后的代码以下所示,固然还有好多其余去除的方法,此处仅仅给出了一种。

   

 

5、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等卫语句。这种语法形式很好的对上述糟糕的形式进行了解决,并且还易于理解。

   

 

6、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 (将函数下移)这两个规则与上述状况相似。就是将上面的字段改为函数,有时候不只字段会出现上述状况,函数也会出现上述状况,须要咱们进行移动。由于使用场景相似,再次就不作过多的赘述了。

 

2、Extract Subclass (提炼子类)

这种状况下用的仍是比较多的,当类中的某些方法只有在特定的类的实例中才会使用到,此时咱们就须要提炼出一个子类,将该方法放到相应的子类中。这样一来咱们的每一个类的职责更为单一,这也就是咱们常说的“单一职责”。

在下方示例中,CustomerBook是一个图书消费者的类。其中customeCharge()方法是普通用户计算消费金额所需的方法,而vipCharge()方法是VIP用户调用的方法,在内部vipCharge()须要调用customeCharege()方法。可是对外部而言,vipCharge()方法只有VIP用户才会用到,在这种状况下咱们就须要使用“Extract Subclass (提炼子类)”规则对VIP进行提炼。

    

 

具体作法是咱们须要提炼出一个子类,也就是说将VIP用户做为普通用户的子类,而后将只有VIP用户才调用的方法放到咱们的VIP子类中。这样一来层次更加明确,每一个类的职责更为单一。上述示例重构后的结果以下所示。

   

与“提炼子类”规则相对应的是“Collapse Hierarchy (折叠继承关系)”。一句话来归纳:就是当你的父类与子类差异不大时,咱们就能够将子类与父类进行合并。将上面的示例翻转就是“Collapse Hierarchy (折叠继承关系)”规则的示例,再次就不作过多的赘述了。

 

3、Form Template Method (构造模板函数)

Form Template Method (构造模板函数)这一规则仍是比较实用的。先说模板,“模板”其实就是框架,没有具体的实现细节,只有固定不变的步骤,能够说模板不关心具体的细节。举个栗子🌰,像前段时间比较火的“秘密花园”,那些没有颜色的线条就是模板,若是一些人获取的是同一本秘密花园,那么说明每一个人所获取的模板是相同的。可是每一个人对每块的区域所图的颜色又有差别,这就是实现细节的不一样。

言归正传,当两个兄弟类中的两个函数中的实现步骤大体一直,可是具体细节不一样。在这种状况下,咱们就能够将大致的步骤提取成模板,放到父类中,而具体细节由各自的子类来实现。具体实现请看下方的类,在Subclass1和Subclass2中的calculate()方法中的大致步骤是相同的,就是对两个值相加,而后返回这两个值的和。可是具体细节不一样,能够看出两个相加值的具体计算方式不一样。

  

 

在上述状况下咱们就能够使用“Form Template Method (构造模板函数)”规则将相同的计算流程进行提取,也就是构造咱们的模板函数。将模板函数放到两个类的父类中,而后在相应的子类中只给出实现细节便可。下方代码段是重构后的代码,父类中多出的方法就是咱们提取的模板函数,而子类中只给出相应的实现细节便可。

  

 

4、以委托取代继承(Replace Inheritance with Delegation

有时候咱们为一些类建立子类后,发现子类只使用了父类的部分方法,并且没有继承或者部分继承了父类的数据。在这种状况下咱们就能够将这种继承关系修改为委托的关系。具体作法就是修改这种继承关系,在原有子类中添加父类的对象字段,在子类中建立相应的方法,在方法中使用委托对象来调用原始父类中相应的方法。

下方示例是咱们假想出来的,可是说明该规则是绰绰有余了。咱们假设SubClass01类中只会用到SuperClass01中的display()方法,而没有继承父类中的数据。在下方示例中是继承关系,在这种状况下咱们须要将其转换成委托关系。

  

下方是咱们重构后的代码,在下方代码中咱们去除了以前的继承关系。并在子类中建立了一个以前父类的代理对象,而且建立了一个相应的方法,在该新建的方法中经过代理对象来调用相应的方法。具体以下所示。

  

上述规则与以继承取代委托(Replace Delegation with Inheritance)原则相对于,使用状况与上述相反,再次就不作过多的赘述了。

代码重构(六):代码重构完整案例

 

不管作什么事情呢,都要有始有终呢。前边连续发表了5篇关于重构的博客,其中分门别类的介绍了一些重构手法。今天的这篇博客就使用一个完整的示例来总结一下以前的重构规则,也算给以前的关于重构的博客画一个句号。今天的示例借鉴于《重构,改善既有代码的设计》这本书中的第一章的示例,在其基础上作了一些修改。今天博客从头至尾就是一个完整的重构过程。首先会给出须要重构的代码,而后对其进行分析,而后对症下药,使用以前咱们分享的重构规则对其进行一步步的重构。

先来聊一下该示例的使用场景(若是你有重构这本书的话,能够参加第一章中的示例,不过本博客中的示例与其有些出入)。就是一个客户去DVD出租的商店里进行消费,下方的程序是给店主用的,来根据用户所借的不一样的DVD种类和数量来计算该用户消费的金额和积分。需求很简单并且也不难理解。今天博客会给出原始的代码,也是须要进行重构的代码。固然原始代码彻底符合需求,而且能够正确执行。废话少说,先看示例吧。

 

1、须要重构的代码

在本篇博客的第一部分,咱们先给出完成上述需求须要重构的代码。而后在此基础上进行分析,使用以前咱们提到过的重构手法进行重构。首先咱们给出了电影类的实现。在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)
复制代码

针对上述案例,上面测试用例的输出结果以下。在每次重构后,咱们都会执行上述测试代码,而后观察结果是否与以前的相同。固然若是你的是单元测试的话,彻底能够把对结果检查的工做交给单元测试中的断言来作。

    

 

2、重构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。

 

3、重构2:将相应的方法移到相应的类中

通过上面的重构,咱们从statement()函数中提取了两个方法。观察这两个重构后的方法咱们不难看出,这两个封装出来的新的方法都须要一个参数,这个参数就是Rental类的对象。也就是这两个方法都依赖于Rental类,而对该函数所在的当前类不太感冒。出现这种状况的缘由就是这两个函数放错了地方,由于这两个函数放在Customer类中不依赖与Customer类而依赖于Rental类,那就足以说明这两个方法应该放在Rental类中。

通过咱们简单的分析后,咱们就能够决定要将咱们新提取的方法放到Rental类中,而且函数的参数去掉。由于函数在Rental类中,因此在函数中直接使用self便可。将计算金额的方法和计算积分的方法移到Rental类中后,咱们的Rental类以下所示。在咱们的Customer中的statement()方法中在计算金额和计算积分时,直接调用Rental中的方法便可。通过这一步重构后,不要忘记执行一下你的测试用例,监测一下重构的结果是否正确。

   

 

4、使用“以查询取代临时变量”再次对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中新添加的方法的调用。

     

通过上面的重构,咱们的方法彷佛是找到了归宿了。重构就是这样,一步步来,不要着急,没动一步老是要向着好的方向发展。若是你从第一部分中的代码重构到第五部分,彷佛有些困难。通过上面这些间接的过程,感受也是挺愉快的蛮。下方是通过咱们此次重构的类图。

   

 

6、使用“多态”取代条件表达式

在咱们以前的博客中对条件表达式进行重构时,提到了使用类的多态对条件表达式进行重构。接下来咱们就要使用该规则对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上进行分享。对每次重构的代码都进行了系统的整理。今天博客中的代码整理的结果以下。

  

相关文章
相关标签/搜索