• 重拾《重构-改善既有代码的设计》


    什么是重构?

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

    为什么要重构?

    • 一个小修改牵涉到了多个地方,且这些点处于未知状态
    • 不易读懂代码(包括读懂自己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(提炼继承体系)
      • 开发封闭原则
        • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
        • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
  • 相关阅读:
    MySQL
    docker-compose部署redis及RabbitMq
    docker-compose部署nacos单机版(简洁优化版)
    用U盘启动安装CentOS的详解
    mysql 获取id最大值
    JAVA编码-- 比较两个BigDecimal大小(重要)
    MYSQL如何把年月日3个int类型的字段拼接成日期类型,并按照日期段进行查询
    Mysql如何根据年月日来查询数据
    springboot 调用redisTemplate时总是为null的解决方法
    shell中read用法
  • 原文地址:https://www.cnblogs.com/aarond/p/Refactoring.html
Copyright © 2020-2023  润新知