重构—改善既有代码的设计9—简化条件表达式

条件逻辑可能十分复杂

decompose conditional:将一个复杂的条件逻辑分成若干个小块。使得“分支逻辑”、“操作细节”分离

consolidate conditional expression:代码中多处测试有相同结果

consolidate duplicate conditional fragment:去掉条件代码中的重复成分

为了让条件表达式也遵循“单一出口原则”,往往向其中加入控制标记。

  不特别在意“一个函数一个出口的教条,使用replace nested conditional with cuard clauses标示出那些特殊情况,使用remove control flag去除那些讨厌的限制

面向对象程序的条件表达式通常比较少(较之过程化程序而言):很多条件行为都被多态机制处理掉了。

  多态的好处:调用者无需了解条件行为的细节=》条件的扩展更容易

replace conditional with polymorphism:将switch语句替换为多态

introduce null object:去除对于null值的校验。多态的一种十分有用且鲜为人知的用途

1.decompose conditional:分解条件表达式

问题:有一个复杂的条件语句(if-then-else)

解决:从if-then-else三个段落中分别提炼出独立的函数

动机:

  复杂的条件逻辑:最常导致复杂度上升的地点之一

    必须编写代码来检查不同的条件分支,根据不同的分支做不同的事情=》导致相当长的函数。

    大型函数自身会使代码的可读性下降+条件逻辑会使代码更难阅读

  在带有复杂条件逻辑的函数中,代码(包括检查条件分支的代码、真正实现功能的代码)会告诉你法僧的事情。但是,常常让人弄不清楚为什么会发生这样的事=》代码的可读性的确大大降低了

  任何大块头代码,都可以分解为多个独立函数。根据每一小块代码的用途,为分解而得的新函数命名,更清楚地表达自己的意图

  对于条件逻辑,每个分支条件分解成新函数:突出条件逻辑,更清楚地表明每个分支的作用,突出每个分支的原因

做法:

  将if-then-else中的每一个段落都提炼出来,构成一个独立函数

    如果发现嵌套的条件逻辑,会先观察是否可以使用replace nested conditional with guard clauses。如果不行,才开始分解其中的每一个条件

注意:

  有的分支条件往往非常短,看上去似乎没有提炼分支的必要。

    尽管这些条件往往很短,在代码意图、代码自身之间往往存在不小的差距。提炼分支能够更好地表达自己的用途,提炼出来的函数可读性更高(就像一段注释那样清楚而明白)

2.consolidate conditional expression:合并条件表达式

问题:有一系列条件测试,都得到相同结果

解决:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立的函数

动机:

  一串条件检查,检查条件各不相同,最终行为却一致

    立即用“逻辑或”、“逻辑与”将它们合并为一个条件表达式

  合并条件表达式的原因:

    1.合并后的条件代码:“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”=》使这一次检查的用意更清晰

      合并前、合并后的代码有着相同的效果,但原先代码传达出的信息:“这里有一些各自独立的条件测试,只是恰好同时发生”

    2.为使用extract method做好准备。将检查条件提炼成一个独立函数,对于理清代码十分有用:把描述“做什么”的语句换成了“为什么这样做”

  不要合并的理由:

    1.如果检查彼此独立,不应该视为同一次检查,就不要使用本重构项

      代码已经清楚表达出自己的意义

做法:

  确定这些条件语句没有副作用:有副作用不能使用本项重构

  使用适当的逻辑操作符,将一系列相关条件表达式合并为一个

  对合并后的条件表达式实施“extract method”,提炼成一个独立函数,并以函数名称表达该语句所检查的条件

    某些情况下,需要同时使用逻辑与、逻辑或、逻辑非,最终得到的条件表达式可能很复杂:先使用extract method将表达式的一部分提炼出来,从而使整个表达式变得简单一些

    如果所观察的部分只是对条件进行检查并返回一个值,就可以使用三元操作符,将这一部分变成一条return语句。

3.consolidate dupulicate conditional fragment:合并重复的条件片段

问题:在条件表达式的每个分支上有着相同的一段代码

解决:将这段重复代码搬移到条件表达式之外。

动机:

  一组体哦阿健表达式的所有分支都执行了相同的某段代码:将这段代码搬移到表达式的外面=》更清楚地表明:那些东西随条件的变化而变化、那些东西保持不变

做法:

  鉴别出“执行方式不随条件变化而变化”的代码

  将共同代码移到条件表达式之外

    如果这些共同代码位于条件表达式的中断=》观察共同代码之前、之后改变了什么。如果的确有所改变,应该首相将共同代码向前、向后移动,移至条件表达式的起始处、尾端。

    共同代码不止一条,首先使用extract method将共同代码提炼到一个独立函数中

注意:

  可以使用同样的手法来对待异常。如果在try-catch都执行了同一段代码,则将这段重复代码移到final区段

4.remove control flag:移除控制标记

问题:在一系列布尔表达式中,某个变量带有“控制标记control flag”的作用

解决:用break 、return语句取代控制标记

动机:

  条件表达式中,常常看到:判断何时停止条件检查的控制标记

    

    带来的麻烦>带来的遍历。

  使用control flag的原因:结构话编程原则,每个子程序只能有一个入口、一个出口

    赞同“单一入口”原则。

    但,“单一出口”原则会在代码中加入讨厌的控制标记,大大降低条件表达式的可读性=》break、continue。用它们跳出复杂的条件语句

  去掉control flag:条件语句的真正用途会清晰很多

做法:

  找出跳出这段逻辑的控制标记值

  找出对标记变量赋值的语句,代以恰当的break、congtinue(对control flag最显而易见的方法)

注意:

  未能提供break、continue语句的编程语言中

    extract method,整段逻辑提炼到一个独立函数中

    找出跳出这段逻辑的控制标记值

    找出对标记变量赋值的语句,代以恰当的return语句

  即使在支持break、congtinue语句的编程语言中,通常也优先考虑上述第二个方案。

    return语句可以非常清楚地表示:不再执行该函数中的其他任何代码

  注意标记变量,是否会影响这段逻辑的最后结果。

    如果有,使用break语句之后还得保留控制标记值。如果已经将这段逻辑提炼成一个独立函数,也可以将控制标记值放在return中返回

  既是控制标记,也是运算结果。将与次变量有关的代码,提炼到一个独立函数中,用return取代控制标记变量

  如果以此办法处理带有,有副作用的函数,会出现问题。先以separate query from modifier将函数副作用分离出去

5.replace nested conditional with guard clauses:以卫语句取代嵌套条件表达式

问题:函数中的条件逻辑使人难以看清正常的执行路径

解决:用卫语句表现所有特殊情况

动机:

  条件表达式,通常有两种情况:

    1.所有分支属于正常行为:应该用if-else-的条件表达式

    2.只有一种是正常行为,其他都是不常见的情况。不常见的条件单独检查;为真时,立刻从函数中返回。

      单独检查,常被称为“卫语句guard clauses”

  精髓:给某一条分支以特别的重视。

    if-then-else结构,对if、else的重视时同等的。

    guard clauses:这种情况很罕见,如果真的发生了,请做一些整理工作,然后退出

  “每个函数只能有一个入口、一个出口”

    编程语言会强制,保证每个函数只有一个入口。

    “单一出口”这个规则,没那么有用。保持代码清晰才是关键。如果单一出口能使函数更清楚易读,就使用单一出口;否则,不必这么做

      嵌套条件代码,往往由那些深心“每个函数只能由一个出口”的程序员写出。此条规则太简单粗暴了。如果对函数剩余部分不再由兴趣,应该立即退出。引导阅读者去看一个没有用的else区段,只会妨碍他们的理解

做法:

  对于每个检查,放进一个guard clauses。卫语句要不从函数中返回,要不就抛出一个异常

    如果所有卫语句都导致相同的结果,请使用 consolidate conditional expressions

注意:

  常常可以将条件表达式反转,从而实现replace nested conditional with guard clauses。

  推荐在guard clauses内返回一个明确的值,可以一目了然地看到guard clauses返回的失败结果。这是也会考虑使用replace magic number with symbolic constant

6.replace conditional with polymorphism:以多态取代条件表达式

问题:条件表达式,是根据对象类型的不同,而选择不同的行为

解决:将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数

动机:

  多态的好处:根据需要对象的不同类型,而采取不同的行为。多态,使你不必编写明显的条件表达式

    类型码的switch语句、基于类型名称的if-then-else语句,在面向对象程序中很少出现

  同一组条件条件表达式在程序许多地点出现,那使用多态的收益是最大的

    使用条件表达式时,如果项添加一种新类型,就必须查找并更新所有条件表达式。

    如果改用多态,只需建立一个新的子类,并在其中提供适当的函数就行了。

    类的用户不需要了解这个子类,就大大降低了系统各部分之间的依赖,使系统升级更容易

做法:

  使用replace conditional with polymorphism之前,首先必须由一个继承结构。

  建立继承结构由两种选择:

    1.replace type code with subclasses:简单,应尽可能使用这一种

    2.replace type code with state/strategy:对象创建之后修改类型码;或者,要重构的类已经有了子类

    如果若干switch语句针对的是同一个类型码,只需针对这个类型码建立一个继承结构就行了

  1.如果要处理的条件表达式是一个更大函数中的一部分。首先,对条件表达式进行分析,然后使用extract method将其提炼到一个独立函数去

  2.如果由必要,使用move method、将条件表达式放置到继承结构的顶端

  3.任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新建函数中,并对其进行适当调整

    可能需要将超类中某些private字段声明为protected

  4.超类中删掉条件表达式内被复制了的分支

  5.将超类中容纳条件表达式的函数声明为抽象函数

7.intruduce null object:引入null对象

问题:需要再三检查某对象是否为null 

解决:将null值替换为null对象

动机:

  多态的最根本好处:不必再向对象询问“你是什么类型”。而后根据得到的答案调用对象的某个行为。只管调用该行为就是了,其他的一切,多态机制会为你安排妥当

  当某个字段内容为null时,多态可扮演另一个较不直观,亦较不为人所知的用途

    解决:每次向一个对象发送一个消息之前,总是要检查对象是否存在。此类检查出现很多次,造成大量的重复代码

    null object==miss object。不让实例变量被设为null,而是插入各式各样的空对象,都知道如何正确的显示自己=》拜托大量过程化的代码

      mock test object:使用此原理。便于模块化开发、测试。

      missing bin:虚构的箱仓。自己不带任何数据,总值为0.

        箱仓:指集合,用来保存某些资薪值,并常常需要对各个资薪值进行加和、遍历

      系统几乎从来不会因为空对象而被破坏。null object对所有外界请求的相应和真实对象一样=》系统行为总是正常的

      并非总是好事儿。有时会造成问题侦测、查找上的困难。因为从来没有任何东西被破坏。

        只要认真检查,就会发现空对象有时出现在不该出现的地方

      空对象一定是常量,它们的任何成分都不会发生变化。可以使用singleton模式来实现=》任何时候,只要请求一个miss**对象,得到的一定是miss**的唯一实例

做法:

  为源类建立一个子类,使其行为就像是源类的null版本。在源类、null子类中都加上isnull(),前者返回false,后者返回true。

    有帮助的做法:建立一个nullable接口,将isnull()放入其中,让源类实现这个接口:昭告大家,这里使用了空对象

    也可以创建一个测试接口,专门用来检查对象是否为null:无法修改null对象的

    工厂函数:专门用来创建null**对象=》用户不必知道空对象的存在

  找出所有“索求对象却获得一个null”的地方。修改这些地方,使其获得一个null对象

  找出所有“将源对象与null作比较”的地方。修改这些地方,使其调用isnull()

    每次只处理一个源对象、其客户程序,编译、测试后,再处理另一个源对象

    可以在“不该出现null”的地方放上一些断言,确保null不再出现

    大多数情况,需要做大量的替换工作(视null对象的使用频率来决定),很凌乱、恼人

    实现nullable接口的对象:(aCustomer is null)来判断

  找出这样的程序点:如果对象不是null,做A动作,否则做B动作

    在null类中覆写A动作,使其行为和B动作相同

    使用被覆写的动作,删除“对象是否等于null”的条件测试

注意:

  只有当大多数(不是所有)客户代码,都要求空对象做出相同响应时,这样的行为搬移才有意义。

    任何用户如果需要空对象做出不同相应,仍然可以使用isnull()函数来测试,只要大多数客户端都要求空对象做出相同响应,就可以调用默认的null行为,自己也就受益匪浅了

  使用本项重构时,可以有几种不同的空对象(例如:没有顾客、不知顾客名的顾客),针对不同情况建立不同的空对象类。

    有时候空对象也可以携带数据:不知名顾客的使用记录……,查出顾客姓名后,将账单寄给他

    本质上是特例模式special case:比null object模式更大的模式。

      某个类的特殊情况,有着特殊行为

      例如:浮点数:有“正无穷大”、“负无穷大”、“NaN”……

      价值:可以降低“错误处理”开销。NaN做浮点运算,结果是个NaN。与“空对象的访问函数通常返回另一个空对象”同理

8.intruduce assertion:引入断言

问题:某一段代码需要对程序状态做出某种假设

解决:以断言明确某种假设

动机:

  只有当某个条件为真时,该段代码才能正常运行。例如:平方根计算只对正值才能进行……

  这样的假设通常并没有在代码中明确表现出来,必须阅读整个算法才能看出。有时会以注释写出这样的假设=》更好的技术:使用断言,明确表明这些假设

  断言是一个条件表达式,应该总为真。如果失败,表示程序员犯了错误

    断言的失败应该导致一个非受控异常unchecked exception。

    断言绝对不能被系统的其他部分使用。

    实际上,程序最后的成品,往往将断言统统删除=》标记“某些东西是断言”,很重要

  断言,可以作为交流、调试的辅助。

    交流角度上,断言可以帮助程序阅读者,理解代码所做的假设(代码正确运行的必要条件)

    调试角度上,断言可以在距离bug最近的地方抓住它们

    =》编写自我测试代码时,断言在调试方面的帮助变得不那么重要了。但,仍要非常看重断言在交流方面的价值

做法:

  如果程序不犯错,断言就应该不会对系统运行造成任何影响=》加入断言,不会影响程序的行为

    如果反写代码假设某个条件始终为真。就加入一个断言,明确说明这种情况

      可以新建一个Assert类,用于处理各种情况下的断言

  不要滥用断言。不要使用它来检查“你认为应该为真”的条件,请只用它来检查“一定必须为真”的条件

    滥用断言,可能会造成难以维护的重复逻辑。

    在一段逻辑中加入断言是有好处的:迫使你重新考虑这段代码的约束条件

    如果不满足这些约束条件,程序也可以正常运行=》断言不会带来任何帮助,只会把代码变得混乱,可能妨碍以后的修改

  应该常常问自己:如果断言所只是的约束条件不能满足,代码是否仍能正常运行?如果可以,将断言删掉

  断言中的重复代码。和其他任何的地方的重复diamagnetic一样不好闻,大胆使用extract method(去重、更清楚说明函数用途)去掉那些重复代码。

注意:

  断言可以被轻松拿掉=》不可能影响最终成品的性能。

  编写一个辅助类(例如:Assert类)是有帮助的。

    缺点:断言参数中的任何表达式不论什么情况,都一定会被执行一遍。

    阻止它的唯一办法:如果Assert.ON是个常量,编译器就会对其进行检查。如果==false,就不会再执行表达式后半段代码

      加此语句有些丑陋,嗯对程序源宁可仅仅使用Assert.isTrue()函数。在项目结束前,过滤掉使用断言的每一行diamagnetic

    

  Assert类应该有很对个函数,函数名称应该帮助程序员理解其功用。

    isTrue()、equals()、shouldNeverReachHere()……

  

  

  

  

原文地址:https://www.cnblogs.com/panpanwelcome/p/7806416.html