架构重构之禅
引言
Refactoring 一直以来都是项目开发中的热点和难点,考虑到更通俗的易懂,本文是什么(what),为什么(why)以及怎么做(how)的三个点进行展开讲解。因为重构不是独立的对某一块代码优化,而是让系统以及代码的相互协调作用表现最佳的改进过程,所以文章的内容可能存在交集的部分,而已理解的情况下,大家可自行跳过。
概括
重构本质
重构是在不创建新的功能方法前提下,改进系统代码的过程,让代码逻辑和架构设计变得更加干净和清晰
重构意旨
重构主要的目的在于解决项目存在的技术债务,将原有的代码和设计更清晰简单,提高系统性能。
代码重构(what)
重构后的代码基本具备以下特征
代码逻辑对于其他项目开发人员清晰简单;
清晰的代码不包括拷贝重复项
干净的代码包括极少的类和方法
代码测试覆盖了100%
技术债务(why)
产生技术债务的原因
业务压力,可能需求迭代的速度要求你必须迅速完成功能上线,而没有时间去完成被忽略的优化工程
你的上级或者雇主不理解技术债务具有“利益”,因为随着债务的累积,技术债务减慢了发展速度。
无法严格按照组件的一致性的规则。通俗的讲就是每个功能的改变可能会影响到其他的组件。
缺乏全面的测试
一个组成员之间缺少相互的交流和反馈
缺少清晰的文档,这样导致新来的员工不能很快的接手项目
一个长期项目存在多个分支同事开发,这样导致了后期项目工程后的合并混乱
重构项目迟迟被延期,无法进行
缺少合理定时的监控
技术开发员工甚至不知道如何或者是否需要重构
何时需要开始重构(How)
三个规则
当你实现一个功能的时候,只需要完成实现该项目工程,而不需要考虑后续的复用性
面临做不愿意重复的项目事情的时候,而不得不重复
当你在做一个项目需要重复第三次的时候,这个时候请重构
在添加新的功能的时候
如果您必须处理别人的脏代码,请尝试首先对其进行重构。干净的代码更容易掌握。您不仅会为自己而且还会为在您之后使用它的人改进它。
重构后如何可以更容易的添加新功能,并且使得代码更加清晰易懂
当修复bug的时候
修复bug的时候,你会自己发现代码中存在的可优化的项目
当你的boss赞扬主动重构的行为时
在code review的时候
code review的时候也许是项目上线发版之前最后一次机会来清楚一些不必要以及不合理的代码
与代码开发者一起评审的时候,这样,您可以快速解决简单的问题,并确定解决更困难的问题的时间
如何重构
在重构之前,请先理解项目并罗列需要重构的清单
请把代码变得更加干净清晰:如果你重构后代码仍然是代码仍然是不干净,模糊不清,对是一些小改的时候,也许看来你是在浪费时间。
新的功能函数不建议在重构中添加,否则你会将代码重构的时候变得混乱
所以的测试必须覆盖重构后100%代码,需要注意的是你重构测试的时候,有两种情况会导致你的测试崩溃:
重复期间不断出现bug,然后继续修复bug
你所写的test是比较低级的测试,而无法覆盖一些特殊场景的测试
重构的具体实现(detail)
代码组织
长方法
产生原因
很多情况,在原有的方法里面添加功能比重现创建一个方法,重现的实现全套方法逻辑要简单的多,因此在项目迭代过程中,我们都会在原来的方法里面里增加需要的功能内容
解决方案
如果方法过长本身内容实现过长的时候,需要将方法里的内容提取出来改成extraMethod方法,这样通过方法调用方法
如果在抽取方法的时候,遇到一些当前作用域下的参数不好提取的时候,可以用查询的方法替换临时变量
如果碰到方法参数过多的情况下,可以将入参组成一个结构体对象。
如果出现循环,请尝试提取方法。对于条件,请使用分解条件。如果出现循环,请尝试提取方法。
性能
很多人会担心如果方法过多,这样会影响性能吗?其实几乎在所以情况下,影响微小的可以忽略不计
大对象
产生原因
与上述长方法一致,对于开发人员来说,将一个新功能放置在现有类中比在该功能上创建一个新类精神上来看更简单和轻松。
解决方案
如果一个类的对象过长,可以像抽离方法一样,通过子类来减轻类的长度;
创建子类,在某些特殊的情况下,类需要实现特殊的方法,而这种情况又是极少的情况下,这个时候可以通过子类来实现,而不是在主类中直接实现过多的方法,并最后可以通过子类中构造方法和或reset()的方法来实现子类属性的重构
如果有必要罗列客户端的行为和使用,这个时候可以通过提取接口的来描述类的功能使用
如果大型类负责图形界面,则可以尝试将其某些数据和行为移至单独的域对象。这样做可能需要将某些数据的副本存储在两个位置并保持数据的一致性。
痴迷于基元使用
常见现象
习惯使用原始类型来代替一些小的对象,譬如:汇率 ,电话号码等
习惯用长量做数字信息 譬如:USER_ADMIN_ROLE = 1
使用字符串常量来作为数组的字段的名称
产生原因
创建原始字段比创建一个全新的类要容易得多
解决方案
创建对象来代替基元(原始类型或者常量),通常我们会推荐创建枚举对象类实现
如果一个常量数组里面包括不一样的数据,我们推荐使用对象代替
在碰到字段需要不同的特殊编码的时候,我们可以将需要处理的字段放入子类,这样对于不同特殊编码的情况,我们可以使用子类的多态特征去实现
如果碰到传参数需要传对象的多个基础属性的时候,请用整个对象代替
优化好处
代码因为使用了对象代替了基元,所以变更和更新变得更加灵活
阅读起来更加清晰易懂,在了解一个逻辑的时候,变得更集中,而不是到处分散的去找基本类型的定义
参数过长
产生原因
过多轮的调用和合并后,在调用新的方法时候,需要之前计算结果当作参数下传
一个方法如果直接传多个参数的话,就更加独立,因为不存在与其他对象的依赖
解决方案
如果碰到传参数需要传对象的多个基础属性的时候,请用整个对象代替
在方法内获取需要查询的字段,而不是通过值传递的方式传入方法
特殊情况
如果因为使用对象而使得必须依赖其他类的时候,请继续使用参数的方式传递
数据块
常见现象
不同代码的地方经常出现了相同的变量的时候,譬如数据库配置,Redis配置的。这个时候,我们需要把相同的数据库可以组织成自己的类。
解决方案
创建静态全局变量,通过使用对象的成员变量来代替原始的数据块
判断语句
常见现象
代码中出现一个方法里面大量的if 或者switch的语句,严重的话这样的判断语句甚至超过了10个以上。
产生原因
在项目跌代中,产品的需求在同一个方向做了不同的逻辑,并且随着版本迭代增加,该方向的个性化逻辑也就逐渐增加
解决方案
将判断的语句if或switch的代码部分独立到一个方法中,通过子方法的方式来处理过长的判断条件的代码块
通过(策略模式+工程模式)或者反射的模式取代,个人认为这个是最好的方式,作者力推荐
设置对象参数的时候,请直接提供清晰的方式,譬如对象的成员变量设置
在需要判断空的条件时候,可以创建子类即空对象来处理
临时变量
产生原因
通常开发人员在局部方法中调用下游的函数,且需要传大量的字段的时候,这个时候他们往往使用局部临时变量。
解决方案
将方法转换为一个单独的类,以便局部变量成为该类的字段。然后,您可以将方法拆分为同一类中的多个方法。
优化好处
代码结构变得更加清晰和容易组织
继承
常见现象
如果子类仅使用从其父类继承的某些方法和属性,则层次结构是不合理的。不需要的方法可以简单地不使用或重新定义并释放异常。
解决方案
使用委派代替继承,即在子类的构造方法中使用超类的委派构造初始化
在超类中已经有的方法,子类如果不存在特定情况下,不需要重复实现;如果针对不同的场景,超类的方法可以定义成未抽象的方法。
具有不同接口,却可相互取代的类
常见现象
通俗的说就是存在两个类,接口名称不一样,但是实现的功能基本一致
产生原因
开发者在创建接口功能的时候,未了解已经存在一个类实现了该接口需要做的功能
解决方案
提高代码可读性,重新定义接口的名称。接口的名称必须能够让其他人简单清楚的了解接口的大致功能
如果接口之前存在一小部分的功能相同,请将重复的部分抽离成一个子方法,供两个接口调用
如果已存在重复的实现后,在确定要使用并实现哪种处理方法之后,您可以删除其中一个重复的类。
面向对象
散布的到处改动
常见现象
有时你只需要给某一个类增加一个字段的时候,而不得不去同时修改其他几个与本次改动不相关的代码。
产生原因
通常这一类现象产生的原因是代码的编写的时候代码结果不良导致
解决方案
对于长类需要将类进行拆分
如果多个类之间存在一样的功能方法或者成员变量,我们可以通过继承来组合这些类。
当一个类的某个方法调用的频率在其他类中比在自己类中还要高的时候,则创建一个新类,把旧类实现的所以方法都转移过去,最后并删除旧类里的这个方法
当然一个类的成员变量使用频率在其他类的地方调用更高的时候,则需要创建一个新的类,把这些成员变量转移过去,最后并删除旧类里的这些成员变量。
重构好处
更好的组织
减少代码重复
易于维护
并行继承的对象类
常见现象
每当为一个类创建一个子类时,就会发现自己需要为另一个类创建一个子类
产生原因
随着项目需求迭代,新的类逐渐增加,后续一些的变动将越来越复杂
解决方案
可以分两步对并行类层次结构进行重复数据删除。首先,使一个层次结构的实例引用另一个层次结构的实例。然后使用上述的“当一个类的某个方法调用的频率在其他类中比在自己类中还要高的时候,则创建一个新类,把旧类实现的所以方法都转移过去,最后并删除旧类里的这个方法”和“当然一个类的成员变量使用频率在其他类的地方调用更高的时候,则需要创建一个新的类,把这些成员变量转移过去,最后并删除旧类里的这些成员变量。”的方式解决
废弃的代码
代码注释
常见现象
一种方法充满过度性的注释
产生原因
当方法创建者意识到自己的代码不直观或不明显时,通常会出于最佳意图创建注释,从而导致了过多的无效注释,如果你觉得没有注释就无法理解代码片段,请尝试以无需注释的方式更改代码结构。
解决方案
最好的注释就是给方法或者类起一个易懂清晰的名字
如果你的注释是为了解释复杂的表达,那么你需要“提取变量”的方式将代码结构改成多个子表达的方式
如果注释解释了一段代码,那么你可以把这段代码抽离成一个方法,而这个新的方法名称本身就是一种有效而简单的注释
如果注释地方的方法已经被抽取了,然而你仍然需要加一些大量的注释的话,这样就需要将方法继续抽离或者重新组织
如果需要声明有关系统正常运行所必需的状态的规则,可以使用断言
重复代码
常见现象
两块代码几乎完全一致
产生原因
当多个程序员同时在同一程序的不同部分上工作时,通常会发生复制。由于他们正在执行不同的任务,因此他们可能不知道自己的同事已经编写了类似的代码,这些代码可以重新用于他们自己的需求。
解决方案
如果一段代码被两个类都在使用的话,请这两个类的这段代码逻辑抽离成一个新的方法,并删除原来两个类的这段代码块,然后将这两个类通过调用新的方法。
如果重复的代码是在类的初始化里出现,那么请创建一个超类的构造函数,完成共同的代码初始化
如果你的代码功能上大体一样 但是又非完全一致的时候,将算法结构和相同的步骤移至超类,并将不同步骤的实现留在子类中
如果两种方法做相同的事情但使用不同的算法,请选择最佳算法并应用替代算法
如果存在大量条件表达式并执行相同的代码(仅在它们的条件中有所不同),请使用合并条件表达式将这些运算符合并为单个条件,然后使用提取方法将条件放在易于操作的单独方法中
如果在条件表达式的所有分支中执行相同的代码:通过使用将相同的代码放在条件树之外
优化效果
合并重复的代码可以简化代码的结构,并使代码结构更短。
代码更易于简化且支持成本更低
使用较少的类
常见现象
理解和维护一个类的时间和成本是非常高的,如果对于一个经常不实用或者很少值得你去额外关注的话,请删除这个类
产生原因
经常一些类被设计为具有全部功能,但经过一些重构后,它变得很小了。也许它旨在支持从未完成的未来开发工作。
解决方案
如果一个类的方法与其他类的功能类似,而且使用的频率很低的话,请直接把功能移至其他类中,并删除本类
对于功能很少的子类,可以直接压缩成一个子类
数据类
常见现象
数据类是指仅包含字段和用于访问它们的原始方法(获取器和设置器)的类。这些只是其他类使用的数据的容器。这些类不包含任何其他功能,并且不能独立地对其拥有的数据进行操作
产生原因
当新创建的类仅包含几个公共字段(甚至可能有少数getter / setter)时,这是很正常的事情。但是对象的真正力量在于它们可以包含行为类型或对其数据的操作。
解决方案
如果一个类包含公共字段,请使用“封装字段”将它们隐藏起来以防止直接访问,并且要求仅通过getter和setter进行访问。
在类里的集合成员变量,将getter返回值设为只读,并创建用于添加/删除集合元素的方法,而不是直接操作当前类的数据元素
对于数据类的方法尽可能私有(数据类往往是存在多线程的访问,所以需要尽可能的不要开放写的权限,否则就给代码维护增加了难度和成本)
数据类只有在初始化的时候,可以设置其值,其他时刻无法修改。(数据类往往是存在多线程的访问,所以需要尽可能的不要开放写的权限,否则就给代码维护增加了难度和成本)
无效的代码
常见现象
不再使用变量,参数,字段,方法或类。
产生原因
当软件要求发生变化或进行了更正时,没有人有时间清理旧代码
解决方案
删!删!删!以前没有svn或git的时候,有些书籍或文章说代码不用注释而不是删除,而现在看来有了版本控制,没用的代码直接删除!
对于不必要的类,如果使用子类或超类,则可以应用内联类或折叠层次结构
如果方法中存在无效的参数的时候也请直接删除
投机代码
常见现象
总有一些用不上的类,方法以及变量或参数
产生原因
有时会以“以防万一”的方式创建代码,以支持从未实现的预期未来功能。结果,创建的代码几乎没有被使用,到后来维护变得难以理解
解决方案
对于很少使用的代码,尝试合并子类或者方法
对于没有必要声明的类可以将其成员变量移动到与其关联的内链类,并删除当前类
对于很少用方法尝试,使用内链方法避免它们
方法用不上的参数,应该直接删除
用不上的成员变量,可以删除
耦合
功能转移
常见现象
当前类方法访问另一个对象的数据比访问其自身的数据更多
产生原因
将过多的字段定义在数据类中,在这种情况下,您可能还希望将对数据的操作移至此类。
解决方案
在使用该方法最多的类中创建一个新方法,然后将代码从旧方法移至该位置。将原始方法的代码转换为对另一个类中新方法的引用,否则将其完全删除
如果仅方法的一部分访问另一个对象的数据,请将这部分代码抽取成方法独立出来,通过调用方式来访问
如果一个方法使用其他几个类的方法,则首先确定哪个类包含大多数使用的数据。然后将方法与其他数据一起放在此类中。或者,使用提取方法将方法分为几个部分,这些部分可以放在不同类别的不同位置
优化效果
减少重复代码
良好的代码组织结构
不合理的组织关系
常见现象
一个类使用另一个类的内部的成员变量和方法
生产原因
开发中把所有的集中点都放到一个类中,这样导致其他的类与该类有了过多的密切联系,譬如依赖该类的方法和该类的成员变量
解决方案
最简单的方法就是拆分类的方法和成员变量,并转移到一个公共的组件,这样类与类之间的之间类就存在较少的依赖
另一种解决的方案就是创建代理,譬如对象A需要方法呢对象B的方法,而不直接访问,而是通过访问大代理客户端,这样对象A就不需要过度关对象B的具体实现和改动
如果两个类是相互依存或者无法剥离,请尝试合并或者归并成一个对象,或者从功能上实现减少不必要的相互依存关系
如果依赖关系是存在子类和超类中的,请直接用继承来代替代理
优化效果
改善代码组织结构
简化代码维护和理解
消息传递链子
常见现象
在代码中,也许经常看到funcA()->funcB()->func()C-funcD(),这样串行的依赖
产生原因
当客户端请求另一个对象,该对象又请求另一个对象,依此类推,就会发生消息链。这些链意味着客户端依赖于沿类结构的导航。这些关系中的任何更改都需要修改客户端
解决方案
删除这样的调用链,在类A中创建一个新方法,该方法将调用委派给对象B(使用代理)
有时最好考虑一下为什么要使用最终对象。也许将抽取方法(具体代码请参考“长方法”)此功能并通过移动方法里的逻辑,将其移动到链的开头是有意义的。有时最好考虑一下为什么要使用最终对象
使用消息中间件来解决这样串行耦合度高的逻辑
优化效果
减少类与类之间的依赖
减少大量无效对的代码
代理
常见现象
如果一个类只有一个简单的方法,调用方式却是通过一个代理类给其他人提供调用
产生原因
盲目过度的想消除类之间的串行调用关系
一个类的功能正在逐步转移到另一个类中,而该类逐渐是一个空壳,而除了代理的作用
解决方案
如果一个方法的大部分的方法都是代理到另一个类的话,删除代类,强直接调用另一个类的最终方法方法
优化效果
减少代码体积
参考文献:https://refactoringguru.cn/store
腾讯云大量岗位职位招聘,欢迎大家来撩:ninetyhe@tencent.com
版权声明: 本文为 InfoQ 作者【ninetyhe】的原创文章。
原文链接:【http://xie.infoq.cn/article/52114564f0aa5aadd1d84aa5f】。文章转载请联系作者。
评论 (2 条评论)