一个项目运行久了,经过业务需求的迭代,开发人员的变更,总会产生一些质量不高的代码,要么来源于对某些业务理解的不太深,要么来源于对一些紧急变更的后遗症,往往遇到这种情况,我们会适时的引入重构,避免破窗效应,让一个项目越来越杂乱。
重构其实不仅可以重新梳理下我们的业务场景,梳理我们代码的逻辑,让其更贴合业务,更重要的是可以让开发人员有机会再次设计我们的系统,结合一些更好的开源项目和技术,提升团队的技术氛围。
每一次重构其实对于一个项目来说都是无比艰难的决定,上有新业务的需求,下有重构的使命,时间紧迫,希望得到很好的效果,压力都会比较大。但是就是这种环境很容易锻炼人,使人飞速成长,通力合作,团队也会得到很好的磨练。
- 首先我们考虑先要梳理项目的痛点,重构其实更重要是解决现在的实际问题。
- 提出更高要求,例如提高项目承载能力,应对更大业务需求。
什么是重构?
- 是在不改变系统行为的前提下,对内部代码的重新组织,提高可理解性和降低修改成本。
为什么要重构?
- 一个小修改牵涉到了多个地方,且这些点处于未知状态
- 不易读懂代码(包括读懂自己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(提炼继承体系)
- 开发封闭原则
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
- 开发封闭原则
欢迎关注 高广超的简书博客 与 收藏文章 !
欢迎关注 头条号:互联网技术栈 !
个人介绍:
高广超 :多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能互联网架构。
本文首发在 高广超的简书博客 转载请注明!