首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >读《重构:改善既有代码的设计》

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

作者头像
高广超
发布2018-12-12 09:35:56
6180
发布2018-12-12 09:35:56
举报
文章被收录于专栏:互联网技术栈互联网技术栈

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

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

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

  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(提炼继承体系)
    • 开发封闭原则
      • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
      • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.04.27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是重构?
  • 为什么要重构?
  • 什么时候需要重构?
  • 重构与性能
  • 重新组织函数
  • 在对象之间搬移特性
  • 重新组织数据
  • 简化条件表达式
  • 简化函数调用
  • 处理概括关系
  • 大型重构
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档