《重构与模式》读书笔记

refactor_to_pattern

GoF的《设计模式》以及Martin Fowler的《重构》都是经典。而《重构与模式》则是将二者结合起来。通过设计模式来指导重构,而重构的目的就是让代码更加的简单,易于维护和扩展。

写作缘由

  • 过度设计:代码存在设计过度的情况
  • 模式万灵丹:认为设计模式的万能的
  • 设计不足:类比与过度设计,长期设计不足,导致开发节奏“快、慢、更慢”
  • 测试驱动开发和持续重构:敏捷开发中,Kent Beck说开发过程是“红-绿-重构”
  • 重构与模式:模式有助于改进设计,通过重构实现模式或者趋向模式进行重构
  • 演进式设计:将模式放到重构的背景中进行领会

什么是重构

定义:重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。
重构过程包括去除重复、简化复杂逻辑和澄清模糊的代码。要保证重构的安全性,必须确保所做的修改不会产生任何破坏,这需要进行手工测试或者自动化测试。循序渐进的进行重构有处于防止增加缺陷。

重构的动机:

  • 使新代码的增加更容易
  • 改善既有代码的设计
  • 对代码理解更透彻
  • 提高编程的趣味性

什么是模式

GoF的《设计模式》是在软件开发中总结出来的用于解决不同场景下的软件开发模式。但是不应该陷入模式痴迷,应保证代码的可读性。
应该知道实现模式的方法不止一种,通过重构是为了实现、趋向和去除模式。通常情况下模式不会使代码更加复杂,实现模式有助于去除重复代码、简化逻辑、说明意图和提高灵活性,但是这也取决于人们对模式的熟悉程度。

代码坏味

  • 1.重复代码(Duplicated Code)

    形成Template Method;用Factory Method引入多态创建;链构造函数;用Composite替换一/多之分;提取Composite;通过Adapter统一接口;引入Null Object;

  • 2.过长函数(Long Method):

    组合方法;将聚集操作搬移到Collecting Parameter;用Command替换条件调度程序;将聚集操作搬移到Visitor;用Strategy替换条件逻辑;

  • 3.条件逻辑太复杂(Conditional Complexity):

    用Strategy替换条件逻辑;将装饰功能搬移到Decorator;用State替换状态改变条件语句;引入Null Object;

  • 4.基本类型偏执(Primitive Obsession):

    用类替换类型代码;用State替换状态改变条件语句;用Strategy替换条件逻辑;用Composite替换隐含树;用Interpreter替换隐式语言;将装饰功能搬移到Decorator;用Builder封装Composite。

  • 5.不恰当的暴露(Indecent Exposure):

    用Factory封装类;

  • 6.解决方案蔓延(Solution Sprawl):

    将创建知识搬移到Factory;

  • 7.异曲同工的类(Alternative Classes With Different Interfaces):

    通过Adapter统一接口;

  • 8.冗赘类(Lazy Class):

    内联Singeton;

  • 9.过大的类(Large Class):

    用Command替换条件调度程序;用State替换状态改变条件语句;用Interpreter替换隐式语言;

  • 10.分支语句(Switch Statement):

    用Command替换条件调度程序;将聚集操作搬移到Visitor;

  • 11.组合爆炸(Combinatorial Explosion):

    用Interpreter替换隐式语言;

  • 12.怪异解决方案(Oddball Solution):

    通过Adapter统一接口;

创建

用Creation Method替换构造函数

如果类中有多个构造函数,那么客户代码就可能不知道该调用哪一个。这个时候,可以使用能够说明意图的返回对象实例的Creation Method来替换构造函数。
优缺点:
+比构造函数能够更好的表达所创建的实例的种类。
+避免了构造函数的局限,比如2个构造函数的参数数目和类型不能相同。
+更容易发现无用的创建代码。
-创建方式是非标准的:有些是用new初始化,有的是用Creation Method实例化。

将创建知识搬移到Factory

当创建一个对象的知识散布在多个类中,说明出现了创建蔓延的问题。应该将有关的创建知识搬移到一个Factory类中。
优缺点:
+合并创建逻辑和实例化/配置选项。
+将客户代码与创建逻辑解耦。
-如果可以直接实例化,会使设计复杂化。

用Factory封装类

如果在同一个包结构中,有实现了同一接口的多个类。可以把类的构造函数声明为非公共的,并通过Factory来创建它们的实例。
优缺点:
+通过意图导向的CreationMethod简化了不同种类实例的创建。
+通过隐藏不需要公开的类减少了包结构的“概念重量”
+帮助严格执行“面向接口编程,而不是面向实现”这一格言。
-当需要创建新种类的实例时,必须新建/更新Creation Method。
-当客户只获得Factory的二进制代码而无法获得源代码时,对Factory的制定将受到限制。

用Factory Method引入多态创建

一个层次中的类都相似的实现一个方法,只是对象创建的步骤不同。可以调用Factory Method来处理实例化的方法的唯一超类版本。
优缺点:
+减少因创建自定义对象而产生的重复代码
+有效的表达了对象创建发生的位置,以及如何重写对象的创建。
+强制Factory Method使用的类必须实现统一的类型。
-可能会向Factory Method的一些实现者传递不必要的参数。

用Builder封装Composite

构造Composite是重复的、复杂的且容易出错的工作。通过使用Builder处理构造细节来简化构造过程。
优缺点:
+简化了构造Composite的客户代码。
+减少了创建Composite的重复和易出错的本性。
+在客户代码和Composite之间实现了松耦合。
-接口可能不会很清楚的表达其意图。

内联Singleton

绝大多数时候,Singleton都是不需要的。当可以设计或重新设计而避免使用它们的时候,Singleton就是不必要的。可以考虑把Singleton的功能搬移到一个保存并提供对象访问入口的类中,并删除Singleton。
优缺点:
+使对象的协作变得更明显和明确。
+保护了单一的实例,且不要要特殊的代码。
-当在许多层次间传递对象实例比较困难的时候,会使设计变得复杂。

简化

组合方法

一个方法逻辑很难理解的时候,就要考虑进行重构。提高每一步骤的可读性。
优缺点:
+清晰的描述了一个方法所实现的功能以及如何实现。
+把方法分解成命名良好的,处在细节的同一层面的行为模块,以此来简化方法。
-可能会产生过多的小方法。
-可能会使调试变得困难,因为程序的逻辑分散在许多小方法中。

用Strategy替换条件逻辑

当一个方法的条件逻辑太多太复杂的时候,考虑使用Strategy模式来为每个分支创建一个Strategy。
优缺点:
+通过减少或去除条件逻辑使算法变得清晰易懂。
+通过把算法的变体搬移到类层次中简化了类。
+允许在运行时用一种算法替换另一种算法。
-当应用基于继承的解决方案或“简化条件表达式”中的重构更简单时,会增加设计的复杂度。
-增加了算法如何获取或接受上下文类的数据的复杂度。

将装饰功能搬移到Decorator

如果需要对一个功能进行增强,那增强的部分就称为装饰功能了。如果直接改代码那是不好的,考虑使用Decorator模式吧。把这个装饰功能放到装饰器内部。
优缺点:
+把装饰功能从类中搬移去除,从而简化了类。
+有效地把类的核心职责和装饰功能区分开来。
+可以去除几个相关类中重复的装饰逻辑。
-改变了被装饰对象的对象类型。
-会使代码变得更难理解和调适。
-当Decorator组合产生负面影响的时候,会增加设计的复杂度。

用State替换状态改变条件语句

如果控制一个对象状态转换的条件表达式过于复杂,那么就使用State模式吧。
优缺点:
+减少或去除状态改变条件逻辑。
+简化了复杂的状态改变逻辑。
+提供了观察状态改变逻辑的很好的鸟瞰图。
-当状态转换逻辑已经易于理解的时候,会增加设计的复杂度。

用Composite替换隐含树

有一些逻辑用原生表示法隐含的形成了树结构。可以使用Composite来进行重构。
优缺点:
+封装重复的指令,如格式化、添加或删除结点。
+提供了处理相似逻辑增长的一般性方法。
+简化了客户代码的构造职责。
-当构造隐式树更简单的时候,会增加设计的复杂度。

用Command替换条件调度程序

许多系统会收到,发送并处理请求。条件调度程序是一条条条件语句,它用来执行请求的发送和处理。可以使用Command模式来实现。
优缺点:
+提供了用统一方法执行不同行为的简单机制。
+允许在运行时改变所处理的请求,以及如何处理请求。
+仅仅需要很少的代码实现。
-当条件调度程序已经足够的时候,会增加设计的复杂度。

泛化

形成Template Method

借助Template设计模式,将算法的不变部分全部由父类实现,而可变的行为留给子类来实现。去掉所有子类的不变部分,如果仍有共同的部分,则继续重构。
优点与缺点:
+通过把不变行为搬移到超类,去除子类中的重复代码
+简化并有效地表达了一个通用算法的步骤。
+允许子类很容易的定制一个算法
-当为了生成算法、子类必须实现很多方法的时候,会增加设计的复杂度。

提取Composite

在处于同一层次的子类中,如果存在完全重复的方法或者部分重复的方法,就可以考虑将这个方法上移到超类。完全重复的方法,直接提取上移。对于部分重复的方法,可以先提取出重复的部分,然后上移。
优缺点:
+去除重复的类存储逻辑和类处理逻辑。
+能够有效的表达类处理逻辑的可继承性。

用Composite替换一/多之分

这个名字,翻译的貌似有点问题。不知道原文是如何写的。简单说,它处理了以下问题:
如果一个类含有2个几乎一样的方法,唯一的区别就是一个用来处理单一对象,一个用来处理对象的集合。那么这个就称为一/多之分。这个时候,我们可以使用Composite进行替换。
使用Composite扩展出And,Or等条件,简化客户端的处理。
优缺点:
+去除与处理一个或多个对象相关联的重复代码。
+提供了处理一个或多个对象的同一方法。
+支持处理多个对象的更丰富的方法。(如OR表达式)
-可能会在Composite的构造过程中要求类型安全的运行时检查。

用Observer替换硬编码的通知

拒绝使用硬编码的通知,考虑使用Observer模式。
优缺点:
+使主题及其观察者访问松散耦合
+支持一个或多个观察者
-当硬编码的通知已经足够的时候,会增加设计复杂度
-当出现串联通知的时候,会增加代码的复杂度
-当观察者没有从他们的主题中被删除的时候,可能会造成内存泄漏。

通过Adapter统一接口

全部满足以下条件,考虑使用Adapter:

  • 2个类所做的事情相同或者相似,但是具有不同的接口。
  • 如果类共享同一个接口,客户代码会更简单、更直接、更紧凑。
  • 无法轻易改变其中一个类的接口,因为它是第三方类库的一部分,或者它是一个已经被其他客户代码广泛使用的框架的一部分,或者无法获得源代码。

比如slf4j,就是通过各种Adapter实现了各种日志系统的大一统,并提供统一的接口。
优缺点:
+使得客户代码可以通过相同的接口与不同的类交互,从而去除或减少了重复代码。
+使客户代码可以通过公共的接口与多个对象交互,从而简化了客户代码。
+统一了客户代码与不同的类的交互方式。
-当类的接口可以改变的时候,会增加设计的复杂度。

提取Adapter

当一个类适配了多个版本的组件,类库,API或其他实体。这个时候,应该为组件,类库,API或其他实体的每个版本提取一个Adapter。
优缺点:
+隔离了不同版本的组件,类库或API之间的不同之处。
+使类只负责适配代码的一个版本。
+避免频繁地修改代码
-如果某个重要行为在Adapter中不可用的话,那么客户代码将无法执行这一重要行为。

用Interpreter替换隐式语言

在前面“用Composite替换一/多之分”部分,提到了使用Composite的问题。但是使用了Composite后,是需要对Composite进行翻译的。否则是不知道如何查询的。
优缺点:
+比隐式语言更好的支持语言元素的组合。
+不需要新的代码来支持语言元素的新组合。
+允许行为的运行时配置。
-会产生定义语法和修改客户代码的开销。
-如果语言很复杂,则需要很多的编程工作。
-如果语言本身就很简单,则会增加设计的复杂度。

保护

用类替换类型代码

字段的类型(如,String或int)无法保护它免受不正确的赋值和非法的等同性比较。使用类进行替代,从而可以限制赋值和等同性比较。
优缺点:
+更好的避免非法赋值和比较。
-比使用不安全类型要求更多的代码。

用Singleton限制实例化

有时间,遇到创建了一个对象的多个实例,导致内存使用过多和系统性能下降。可以使用单例模式。慎用。
优缺点:
+改进性能。
-从任何地方都可以很容易的访问。在很多情况下,这可能是设计的缺点。
-当对象含有不能共享的状态的时候,本重构就无效了。

引入Null Object

代码中到处都是处理null字段或变量的重复逻辑。而使用Null Object可以进行改善。
优缺点:
+不需要重复的null逻辑就可以避免null错误。
+通过最小化null测试简化了代码。
-当系统不太需要null测试的时候,会增加设计的复杂度。
-如果程序员不知道Null Object的存在,就会产生多余的null测试。
-使维护变得复杂。拥有超类的Null Object必须重写所有新继承到的公共方法。

聚集操作

将聚集操作搬移到Collecting Parameter

有一个很大的方法将信息聚集到一个局部变量中,那么可以把结果聚集到一个Collecting Parameter中,将它传入被提炼出的方法。
优缺点:
+帮助我们把很大的方法转换成更小的,更简单的多个方法。
-使结果代码运行得更快。

将聚集操作搬移到Visitor

如果一个方法需要从不同的类中聚集信息,可以考虑把聚集工作搬移到一个能够访问每个类以便聚集信息的Visitor中。
优缺点:
+调节多个算法,使其适用于不同的对象结构。
+访问相同或不同继承结构中的类。
+调用不同类上的类型特定方法,无需类型转换。
-当可以使用通用接口把互不相同的类变成相似类的时候,会增加代码的复杂度。
-新的可访问类需要新的接受方法,每个Visitor中需要新的访问方法。
-可能会破坏被访问类的封装性。