读《重构:改善既有代码的设计》

一个项目运行久了,经过业务需求的迭代,开发人员的变更,总会产生一些质量不高的代码,要么来源于对某些业务理解的不太深,要么来源于对一些紧急变更的后遗症,往往遇到这种情况,我们会适时的引入重构,避免破窗效应,让一个项目越来越杂乱。

重构其实不仅可以重新梳理下我们的业务场景,梳理我们代码的逻辑,让其更贴合业务,更重要的是可以让开发人员有机会再次设计我们的系统,结合一些更好的开源项目和技术,提升团队的技术氛围。

每一次重构其实对于一个项目来说都是无比艰难的决定,上有新业务的需求,下有重构的使命,时间紧迫,希望得到很好的效果,压力都会比较大。但是就是这种环境很容易锻炼人,使人飞速成长,通力合作,团队也会得到很好的磨练。

  1. 首先我们考虑先要梳理项目的痛点,重构其实更重要是解决现在的实际问题。
  2. 提出更高要求,例如提高项目承载能力,应对更大业务需求。

什么是重构?

  • 是在不改变系统行为的前提下,对内部代码的重新组织,提高可理解性和降低修改成本。

为什么要重构?

  • 一个小修改牵涉到了多个地方,且这些点处于未知状态
  • 不易读懂代码(包括读懂自己1个月前的代码)
  • 新手修改代码上手慢,需要很久才能进行有信心的代码修改
  • 需求变化时,代码层面响应慢

什么时候需要重构?

  • 随时随地的重构,也就是从一开始就进行小范围的重构,就不至于时间久后没法平滑的重构了
  • 上面这句实际上是个方法论级别的,真实中,还是没办法判断什么时候要进行重构,于是换成:当代码中出现了坏味道时需要重构
  • 什么是坏味道:
    • 存在重复代码时
    • 函数体太长
    • 函数参数太长
    • 无法直观的看出代码逻辑
    • 类太大
    • 对一个常量存在了多个副本
    • 很多很多的if/else/switch语句
    • 类名、函数名、方法名不友好

重构与性能

  • 重构为先,调优其次
  • 重构能组织良好的结构,良好的结构能让调优工作更轻松

重新组织函数

  • Extract Method(提炼函数)
    • 当内部逻辑过分缠绕在一起时,需要将一些代码抽取到子函数中
  • Inline Method(内联函数)
    • 如果一个函数体很少,并且没有被其他函数使用到,就可以考虑将这个小函数内联到父函数中
  • Inline Temp(内联临时变量)
    • 如果一个变量只被使用到了1次,并且这个变量所代表的逻辑很少,此时可以考虑将这个临时变量所代表的逻辑直接拷贝到父函数中
  • Replace Temp with Query(以查询取代临时变量)
    • 如果去除了临时变量后,更加利于后续的重构改动,则会使用这种方法,将临时变量所代表的逻辑抽取成单独一个函数
    • 虽然对性能有影响,但是重构过去后,如果不是很严重的性能影响,则还是建议改成这样,因为重构过去后对后续重构更有利,更便于以后的重构
  • Introduce Explaining Variable(引入解释性变量)
    • 将逻辑碎片赋给命名友好的变量名,这样代码的可读性、理解性更强
  • Split Temporary variable(分解临时变量)
    • 一个逻辑目的只赋给一个临时变量,不要合用临时变量,如:
int temp=x+y;
//some logic to process temp varialbe
temp=getBase()+100;
//some logic to process the new temp varialbe
  • Remove Assignments to Parameters(移除对参数的赋值)
    • 禁止对传入参数的赋值,要用增加临时变量的方式来
  • Replace Method with method Object(以函数对象取代函数)
    • 针对大函数、逻辑复杂、局部变量多时
    • 思想是将这个函数独立成为一个类,在类中进行复杂逻辑的处理
  • Substitute Algorithm(替换算法)
    • 将函数内部的算法替换掉,比如:为了更高的效率或者更好的可理解性
    • 意图是提升效率或者可理解性
  • 大方向上都是让语义更加清晰

在对象之间搬移特性

  • Move Method(搬移函数)
    • 如果发现某个函数主要依赖于其他类的数据,则有必要将这个函数move到那个类中
  • Move Field(搬移字段)
    • 和上面的类似,至于是用哪个方法重构,需要看情况,比如看类的名称、职责定义
  • Extract Class(提炼类)
    • 当类包含大量函数、数据时,需要考虑拆分类
  • Inline Class(将类内联化)
    • 当某个类的职责不足以成为一个类时,考虑将这个类合并到其他类中
    • 比如这种情况发生在重构行为后,弱化了某个类的职责
  • Hide Delegate(隐藏“委托关系”)
    • 在server端隐藏某个类,这样客户端只需要知道1个类就能做逻辑操作,而不需要同时知道多个类才能进行逻辑操作了
  • Remove Middle Man(移除中间人)
    • 暴露更多的类来供客户端调用
    • “中间人”的移除与否比较难定,一般模块之间是尽量少暴露,模块内部要看情况而定
  • Introduce Foreign Method(引入外加函数)
    • 当提供的函数不能修改时,可以在客户端增加一个函数来包装这个目标函数,完成额外逻辑的插入转换
    • 这种额外函数不多
    • 用多了不好,最终需要合并到目标函数所在的server端
  • Introduce Local Extension(引入本地扩展)
    • 如果发生上述情况,并且扩展的比较多,则可以在客户端新建一个类,通过继承或者Wrapper的方式导入原始方法或类,进行额外方法、函数、逻辑的加工

重新组织数据

  • Self Encapsulate Field(自封装字段)
    • C#中使用属性来解决,不引用字段,要引用属性,以便在需要覆写变量值的时候嵌入逻辑
  • Replace Data Value with Object(以对象取代数据值)
    • 当对某个基元数据有更多的普遍常用功能时,需要将基元数据替换为对象类型,进而在这个对象中实现一些常用功能,方便调用方的调用
  • Change Value to Reference(将值对象改为引用对象)
    • 如果当前的某个值对象被多个地方用到,并且此时希望更改了一处后,其他地方的引用也跟着改变,此时需要将这个值对象转换为引用对象
    • 场景:项目刚开始时用了值对象,但是后来认为用引用类型更好,此时就需要转换
  • Change Reference to Value(将引用对象改为值对象)
    • 如果存在一个引用类型,而且这个引用类型较小,且不需要实现实例间的互相更改,此时可以把这个引用类型改为值类型,这样能保证这个对象的不可变性
  • Replace Array with Object(以对象取代数组)
    • 当一个数组被用在了传递对象属性用途时,可以采用类来替代这个数组
  • Duplicate Observed Data(复制“被监视的数据”)
    • 层与层之间的缠绕调用,没有划分好层导致的
    • 层与层之间通过DTO的方式进行传输数据
  • Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
    • 谨慎使用,尽量使单向关联
    • 需要在双方对象中加入维护对方的代码,如:Customer.AddOrder/Order.AddCustomer,都要成对出现
  • Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
    • 随着需求的演化,在某时间段,发现不需要双向关联了,此时用此法
  • Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
    • 字面量需要用const常量来替代
    • 如科学计算中某些具有特殊意义的数值,需要统一const引用
  • Encapsulate Field(封装字段)
    • 数据和行为被分开后,由于谁都可以引用public数据,因此不容易管理及修改
    • 如果不暴露数据,这样就能做到只在当前class中使用这些数据了
  • Encapsulate Collection(封装集合)
    • 默认的List<T> Collection<T> ArrayList暴露了太多内部逻辑,而且返回的对象能够被客户端修改,不利于隔离与封装
    • 自己写集合类,可以只暴露特定接口、返回对象新的拷贝,这样能解决恶意、无意的修改
  • Replace Record with Data Class(以数据类取代记录)
    • 将非对象化的平面数据类型(如:数组、传递过来的没有良好命名的属性等),重写成class,只有private属性的class
    • 目的只是为以后更进一步的重构做准备
  • Replace Type Code with Class(以类取代类型码)
    • Type Code:枚举、多个string、int变量,如:string Male="男性" string Female="女性"),诸如此类的标识
    • 将这个Type Code(包含了多个字段,但是只是区分不同的Type)抽象为一个Type Code类
    • 引用的相关地方也要做出更改
  • Replace Type Code with Subclasses(以子类取代类型码)
    • 用子类来标识,这样可以使用重写函数来解决一些行为上的变化
  • Replace Type Code with State/Strategy(以State/Strategy取代类型码)
    • 用状态、策略模式将变化部分抽取出来
  • Replace Subclass with Fields(以字段取代子类)
    • 如果子类中只是简单的返回一些常量,则可以将这些子类废除,压缩继承级别,将类型判断的逻辑写在父类的相应方法中

简化条件表达式

  • Decompose Conditional(分解条件表达式)
    • 往往逻辑比较复杂的地方,分支就较多
    • 一个分支中如果写了很多小段代码,也应该重构成更有语义的代码
    • 需要将分支重构为更加语义化,这样会提高可读性
  • Consolidate Conditional Expression(合并条件表达式)
    • 一般在函数入口出会检查参数有效性,如果写有多条if语句判断为无效,都返回false,则可以将这些都return false的判断抽取到一个单独函数中
    • 主函数中语义更加清晰
  • Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
    • 如果在if/else分支中,每个分支的开始或者结束区域都使用了同样的代码,则提取到if/else外进行统一调用
  • Remove Control Flag(移除控制标记)
    • 用在循环中,去掉控制标记,比如bool found=false之类的控制标记,当找到时,直接return obj/return;
  • Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
    • 把if/else以及嵌套的if/else改成平面写法,如:
if(xxx)return result+1;
if(yyy)return result+2;
if(zzz)return result+3;
return result+4;
  • Replace Conditional with Polymorphism(以多态取代条件表达式)
    • 用在有多个子类的继承体系中,父类有个方法用来计算:根据不同的子类来计算不同的value
    • 套用模板方法设计模式一样
  • Introduce Null Object(引入Null对象)
    • 针对null对象的设计模式
    • 可以将null时,业务逻辑的例外算法在NullObject中实现一份,这样在业务逻辑类中就不需要些一堆if null之类的判断以及转发了
  • Introduce Assertion(引入断言)
    • 在函数的入口编写Assert,用来确保被调用此函数时,相应的前置条件是否正确,使用
    • 如果断言失败,则会在日志文件中出现调用堆栈信息以及自定义信息
    • System.Diagnostics.Trace.Assert:无论是否Release,都会记录日志
    • System.Diagnostics.Trace.Debug:只在Debug模式下生成日志信息

简化函数调用

  • Rename Method(函数改名)
  • 修改函数命名为更有语义,提高可读性
    • 参数顺序、参数命名也是考虑之一
  • Add Parameter(添加参数)
    • 修改了一个函数,但是这个函数目前又需要用到以前所没有的信息
  • Remove Parameter(移除参数)
    • 以前的参数,现在不需要了
  • Separate Query from Modifier(将查询函数和修改函数分离)
    • 如果一个函数在返回值的过程中,也去修改了一些值,则会对客户端调用者产生某些困扰,需要将其拆分为2个函数:Query、Modify
  • Parameterize Method(令函数携带参数)
    • 在函数内部提取公用子函数,来实现代码的扁平化及公用化
  • Replace Parameter with Explicit Methods(以明确函数取代参数)
    • 当函数行为完全取决于参数value时,需要将这个函数拆分到多个方法,避免函数内部逻辑太杂
  • Reserve Whole Object(保持对象完整)
    • 当被调用函数的参数正好是某对象的其中几个属性时,则直接传入这个对象
    • 需要同时考虑被调用函数是否需要move到这个对象中
  • Replace Parameter with Methods(以函数取代参数)
    • 如果主函数中包含有多个子函数,并且这些子函数返回值只是首尾传入传出
    • 此时,考虑将除最后一个函数外,其他子函数不通过主函数来调用,而是通过最后一个字函数的内部进行调用
  • Introduce Parameter Object(引入参数对象)
    • 当某些参数总是成对、成堆出现时,考虑此模式
      如:
DateTime from, DateTime end==> DateRange
int pageIndex, int pageSize==>PagingInfo
以及PagingResult<T>{TotalCount, List<T>}
  • Remove Setting Method(移除设值函数)
    • 如果某个类的属性在构造后就不需要被改变,则把相应的set访问器关闭
  • Hide Method(隐藏函数)
    • 如果某函数没有被其他类引用到,就改成private的
  • Replace Constructor with Factory Method(以工厂函数取代构造函数)
    • 当类存在多个子类,并且希望通过类型码来生成新对象时,可以将构造函数改成工厂方法,这样便于客户端调用,无需知道到底是哪个子类
  • Encapsulate Downcast(封装向下转型)
    • 是说对于类型的强制转换,需要放在具体的函数中实现,不要放在客户端代码中
    • 现在.Net有了泛型,减少了很多这种麻烦
  • Replace Error Code with Exception(以异常取代错误码)
    • 在代码中如遇异常,则直接throw new XXXXException("xx"),而不是用return errorCode的方式
    • 如果是可控异常,则在catch(XXXException ex)处理掉
    • 如果是不可控异常,则无需处理
    • 不可控异常应有框架来处理,如AOP或者Global中的Error事件
  • Replace Exception with Test(以测试取代异常)
    • 对于滥用了catch异常的逻辑进行逻辑上的修改
    • 用单元测试+Assert+边界值测试来确保某些异常没有被触发

处理概括关系

  • Pull Up Field(字段上移)
    • 当多个子类中存在相似的字段时,需要分析下是否需要将这些相似的字段提取到父类中
  • Pull Up Method(函数上移)
    • 当多个子类中存在相似的函数时,需要分析下是否需要将这些相似的函数提取到父类中
    • 如果完全相同,那就直接提取到父类
    • 如果只是某个步骤不通,则通过模板方法把公用逻辑提升到父类中
  • Pull Up Constructor Body(构造函数本体上移)
    • 子类中的构造函数尽量利用父类的构造函数来赋值
  • Pull Down Method(函数下移)
    • 当父类中的某个函数只与某几个子类(非全部)有关时,则将这个函数下放到具体的子类中实现
  • Pull Down Field(字段下移)
    • 当父类中的某个字段只与某几个子类(非全部)有关时,则将这个字段下放到具体的子类中
  • Extract Subclass(提炼子类)
    • 当存在Type Code时,或者当类的某些instance存在不一样的行为时,需要提炼子类
    • 类的某些特性只被某些instance用到
  • Extract Superclass(提炼超类)
    • 如果多个类之间存在相似的特性,则可以新增一个超类将共性提取出来
  • Extract Interface(提炼接口)
    • 直接引用一个类,会将所有的方法暴露出来
    • 如果根据职责定义接口,再让类实现这些接口,调用时的封装、隐蔽性就会好很多
  • Collapse Hierarchy(折叠继承体系)
    • 当父类与子类之间的区别不大时,可以将它们合并,去掉层级关系
  • Form Template Method(塑造模板函数)
    • 其实就是模板设计模式的应用
  • Replace Inheritance with Delegation(以委托取代继承)
    • 当子类发现实际不需要使用集成来的数据、函数时,或者只用到了少数父类的数据、函数时,则可以去掉继承关系,在当前类中加上父类引用,通过委托方式来调用父类的数据、功能
  • Replace Delegation with Inheritance(以继承取代委托)
    • 当2个类之间使用了很多委托来进行调用,并且这些委托覆盖面为对方的大范围时,考虑将委托改成继承关系

大型重构

  • Tease Apart Inheritance(梳理并分解继承体系)
    • 桥接模式的分割
  • Convert Procedural Design to Objects(将过程化设计转化为对象设计)
    • OO对象的建立
    • 职责的分离
  • Separate Domain from Presentation(将领域和表述/显示分离)
    • MVC模式
    • MVVM模式
    • View与Domain的区分
  • Extract Hierarchy(提炼继承体系)
    • 开发封闭原则
      • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
      • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

欢迎关注 高广超的简书博客 与 收藏文章 !
欢迎关注 头条号:互联网技术栈

个人介绍:

高广超 :多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能互联网架构。

本文首发在 高广超的简书博客 转载请注明!

读《重构:改善既有代码的设计》
image.png
上一篇:使用Hystrix实现自动降级与依赖隔离[微服务]


下一篇:Java结构型模式(1)适配器模式