标签:重构
Refactoring
名词:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低修改成本。
动词:使用一系列重构方法,在不改变软件可观察行为的前提下,调整其结构。
改进软件设计
提高代码质量和可读性,使软件系统更易理解和维护
帮助尽早的发现缺陷
提高编程速度
何时重构:
1)随时随地进行。
2)三次法则:第一次做某件事只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次 再做类似的事情,就应该重构了。
3)添加功能
4)修复bug
5)复审代码,即Code Review时候
何时不该重构:
1)代码是在太混乱了,设计完全错误
2)如果项目已近最后期限,应该避免重构
3)重构还不如重新编码
1)读懂代码(包括测试代码)
2)进行重构
3)运行所有的单元测试
重构与设计是互补的,程序应该先设计,但在开始编码后,设计上的不足可以用重构来弥补。设计应该是适度的设计,而不必过度设计。如果能很容易的通过重构来适应需求的变化,那么就不必过度的设计,当需求改变时再重构代码。
1) 如果你发现自己需要为程序添加一个功能,而代码的结构使你无法很方便地达到目的,那就先重构那个程序,使功能的添加比较容易进行,然后再添加那个功能。
2) 重构前,先检查自己是否有一个可以依靠的测试机制。这些测试必须有自我检验的能力。
3) 重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
4) 任何一个傻瓜都能写出计算机可以理解的代码。唯有写出让人能够容易理解的代码才是真正优秀的程序员。
5) 事不过三,三则重构。
6) 不要过早发布接口。谨慎修改你代码的各种规范,使重构更顺畅。
7) 当你感觉需要撰写注释时,先尝试重构,试着让注释显得多余。
8) 确保所有测试都完全自动化,让它们检查自己的测试结果。
9) 一套测试就是一个强大的bug检测器,能够大大缩减查找bug所需要的时间。
10) 频繁地进行测试。每次编译请把测试也考虑进去——每天至少执行每个测试一次。
11) 每当你收到bug报告,请先写一个单元测试来暴露这这个bug。
12) 编写未臻完善的测试并实际运行,好过对完美测试的无尽等待。
13) 考虑可能出错的边界条件,把测试火力集中在那儿。
14) 当事情被大家认为应该会出错时,别忘了检查是否抛出了预期的异常。
15) 不要因为测试无法捕获所有bug不就写测试,因为测试的确可以捕捉到到大多数bug。
1. Duplicated Code:重复代码
指不同的地方出现相同的程序结构。
重构方法:Extract Method、Extract Class、Pull Up Method、Form Template Method
2. Long Method:过长函数
重构方法:Extract Method、Replace Temp with Query、Replace Method with Method Object、Decompose Conditional
3. Large Class:过大的类
重构方法:Extract Class、Extract Subclass、Extract Interface、Replace Data Value with Object
4. Long Parameter List:过长参数列表
重构方法:Replace Parameter with Method、Introduce Parameter Object、Preserve Whole Object
5. Divergent Change:发散式变化
一个类经常由于不同的原因向不同的方向变化。实际上是违法了单一职责原则。
重构方法:Extract Class
6. Shotgun Surgery:霰弹式修改
一种变化会导致好几个类的修改。
重构方法:Move Method、Move Field、Inline Class
7. Feature Envy:依恋情结
类中的方法对另一个类的访问超过了对所在类的访问。
重构方法:Move Method、Move Field、Extract Method
8. Data Clumps:数据泥团
指喜欢经常成群出现的多个数据项,如两个类中相同的字段,或者许多方法中相同的参数签名。
重构方法:Extract Class、Introduce Parameter Object、Preserve Whole Object
9. Primitive Obsession:基本类型偏执
热衷于使用int,long,String等基本类型而不是有意义的小对象。
重构方法:Replace Parameter with Method、Extract Class、Introduce Parameter Object、Replace Array with Object、Replace Type Code with Class、Replace Type Code with Subclass、Replace Type Code with State/Strategy
10. Switch Statement:Switch语句
相同的Switch语句散落在各处,需要添加一个case时需要修改多个地方。
重构方法:Replace Conditional With Polymorphic、Replace Type Code with Subclass、Replace Type Code with State/Strategy、Replace Parameter with Explicit Methods、Introduce Null Object
11. Parallel Inheritance Hierarchies:平行继承体系
如果一个类增加一个子类,另一个类也必须相应的增加一个子类。
重构方法:Move Method、Move Field
12. Lazy Class:冗余类
无用的类
重构方法:Inline Class、Collapse Hierarchy
13. Speculative Generality:夸夸其谈未来性能
指无用的抽象类,无用的预留参数等当前没有用到的部件。这个往往是过度设计的结果。
重构方法:Collapse Hierarchy、Inline Class、Remove Parameter、Rename Method
14. Temporary Field:令人迷惑的临时字段
指仅在特定环境下使用的实例变量,比如为了方便某个方法调用而在对象中添加的字段,该字段仅在这个方法中有用。
重构方法:Extract Class、Introduce Null Object
15. Message Chains:过度耦合的消息链
指客户向一个对象索取另一个对象,然后在向后者索求另一个对象,然后在索求另一个对象——客户与查找过程的紧密耦合。
重构方法:Hide Delegate
16. Middle Man:中间人
过度委托,一个类中的大部分方法都委托给其他类。
重构方法:Remove Middle Man、Inline Method、Replace Delegation with Inheritance
17. Inappropriate Intimacy:狎昵关系
两个类大量探究彼此的private部分。
重构方法:Move Method、Move Field、Change Bindirectional Association to Unidirectional、Replace Inheritance with Delegation、Hide Delegate
18. Alternative Classes with Different Interface:异曲同工的类
类名不同但功能相似。
重构方法:Rename Method、Move Method
19. Incomplete Library Class:不完善的类库
指现有的功能不能满足要求。
重构方法:Introduce Foreign Method、Introduce Local Extension
20. Data Class:纯稚的数据类
指只有公共成员变量,或只有字段和get/set方法的类。
重构方法:Move Method、Encapsulate Field、Encapsulate Collection
21. Refused Bequest:被拒绝的遗赠
指子类只使用了父类的部分方法。
重构方法:Replace Inheritance with Delegation
22. Comments:过多的注释
过多的垃圾注释,或者代码太复杂,必须使用注释说明。
重构方法:Extract Method、Introduce Assertion
将一段独立的代码抽取到一个新的方法,并以它“做什么”来命名。
无局部变量:直接抽取;
局部变量在抽取的方法中只读:以参数传递;
局部变量在抽取的方法中被赋值:
如果变量只在抽取的代码中使用:将变量的声明也抽取到方法中
抽取的代码之外也使用了这个变量:
抽取的方法之后没有使用:在抽取的方法中直接修改
抽取的方法之后有使用:返回值返回
如果需要返回的代码超过一个,则暂不抽取
如果一个方法的实现很简单,并且实现本身和方法的名称一样通俗易懂,则直接在方法的调用点用方法的实现代替方法调用。如果方法被子类覆写,或者涉及递归等复杂的情况,则暂不重构。
如果一个临时变量只被一个简单的表达式赋值,而且这个临时变量妨碍了其他重构手法,则将对这个临时变量的引用替换为这个赋值表达式。使用该方法要确保赋值表达式没有副作用,而且保证变量后面没有被赋值(可以通过将变量声明为final测试)。
如果一个临时变量保存某一个(组)表达式的计算结果,则将表达式提炼为一个方法,这个新方法也可以被其他方法调用。
将复杂表达式(或其中一部分的)结果保存到一个临时变量,并以它的用途命名该临时变量,提高可读性。
如果一个临时变量被赋值超过一次,而且它不是循环变量,也不用于收集计算结果,则针对每次赋值创造一个新的临时变量,提高可读性,避免混淆。
不要对方法传递进来的参数赋值,尤其是混用值传递和引用传递的时候,会降低代码清晰度,应该建一个临时变量,并将参数传递给它,如果确实需要修改传递进来的值,请使用返回值。
如果代码使用了很多临时变量,无法进行方法抽取,则这个方法变成一个新的类,那么方法的临时变量就会变成新类的实例域,然后就可以在该类中将这个大方法分解成多个小方法,而旧的方法调用者改成新建一个类对象,并调用相应的方法。
如果两个算法做从相同的事情,则保留清晰的那个算法。
如果一个类中某个方法跟另一个类的交互更多,则这个方法可能放错了地方,考虑迁移到那个类。
如果一个类中的某个字段更多的被另一个类使用,则这个字段可能放错了地方,考虑迁移到那个类。
某个类做了两个类应该做的事情,比如某些字段经常一起变化,某些字段和某些方法总是一起出现,则考虑将它们提取出来,形成一个新的类。
如果一个类没有做太多的事情,则将考虑将其特性迁移到另一个类,并将其删除。
如果一个类A经常需要通过其委托类B来调用另一个类C,则在委托类B中建立委托方法,类A通过调用B的委托方法访问类C。
如果一个类做了太多简单的委托操作,则删除这个类,让客户端直接调用受托类。
当需要为类新加一个方法,但无法修改该类时,可以新建一个方法,并将该类的对象作为参数传递进去,在这个方法中完成新的功能。
当需要为类新加一个方法,但无法修改该类时,可以让一个新类继承这个类,或者成为这个类的包装类,并在这个新类中实现新的功能。
如果方法名称未能正确表明方法的用途,则重命名方法
如果方法需要从调用端得到更多的信息,则添加参数
如果方法不再需要某个参数,则删除它
如果一个方法中既查询了一个对象,也修改了一个对象,则建立两个方法,一个负责查询,一个负责修改。
如果几个方法做了类似的操作,只是数据不同,则将这几个方法合并为一个方法,并用参数表达那些不同的值。
如果有一个方法,完全根据不同的参数值采用了不同的行为,则将这个方法拆分,为参数的每一个可能的值建立一个方法。
如果有个方法的参数全部来自于一个对象,则改为使用这个对象作为参数。
对象调用某个方法,并将其作为另一个方法的参数,如果另一个方法也能调用这个方法,则去除参数,直接让其调用这个方法。
如果某些参数总是一起出现,则使用一个类对象来代替这些参数,可以减少参数数量。
如果类中的某字段应该在创建时设置,此后不再变化,则删除set方法。
如果一个方法从来没有被其他方法使用,则改为private
如果创建对象不是仅仅做简单的对象构建,如根据不同的参数进行不同的构建,或者需要控制对象的实例个数,则使用工厂方法取代构造器
如果某个方法返回的对象需要调用者执行向下转型,则将转型动作移动到方法中。
对于返回特定的值(如-1)表示错误的方法,考虑改为抛出异常,调用者不一定会检查返回值。
对于一个可以预先检查的条件,不要抛出异常,而应该用if先对其进行检查。
如果有一个复杂的if-then-else语句,则将if、then、else语句提取为单独的方法。
如果有一系列的条件执行相同的逻辑,则将这些条件提炼为一个返回Boolean的独立方法,然后在一个条件测试语句中调用这个方法。
如果在条件表达式的每个分支上都有一段相同的代码,则将这段代码提取到条件表达式之外
在一系列Boolean表达式中,如果某个变量带有控制标记的作用,则移除这个变量,使用break或者return语句代替。
条件表达式通常有两种表现形式,第一种形式是:所有分支都属于正常行为,第二种形式则是条件表达式提供的答案中只有一种是正常行为,其他都是不正常见的情况。如果两条分支都是正常行为,就应该使用if else。如果某个分支极其罕见,就应该单独检查该条件,并在该条件为真时从函数返回,这样的检查就是传说中的“卫语句”。
如果表达式根据不同对象类型选择不同的行为,则将这个表达式的每个分支放到一个子类内的覆写方法中,然后将原始的方法声明为抽象方法。
需要再三检查某物是否为null值,则考虑将null值替换为null对象(无效对象)。
如果某一段代码需要对程序状态做出某种假设,则以断言明确表现这种假设
如果子类有相同的字段,则迁移到父类
如果子类有功能相同的方法,则迁移到父类
将子类构造方法体中相同的部分提取到父类的构造方法,子类使用super调用这个构造方法。
将父类中只与部分子类相关的字段迁移到这些子类。
将父类中只与部分子类相关的方法迁移到这些子类。
将类中只被某些实例用到的特性迁移到一个新的子类中。
如果两个类有相似的特性,则新建一个类作为这两个类的父类,并将那些相似的特性移动到这个新建的父类。
如果若干客户使用类接口中的同一子集,或者两个类有相同的部分,则将相同的子集提取到一个接口中。
如果子类和超类没有太大的差别,则合并到一个继承层次中。
类似模板方法模式,如果子类中以相同的顺序执行某些类似的方法,则将操作顺序提取到一个方法,并放到父类,父类调用这些不同的操作,子类通过覆写这些方法实现差异。
就是使用组合代替继承。如果子类只使用超类接口的一部分,或者根本不需要继承而来的数据,则删除子类对父类的继承,而是在子类中新建一个超类对象。
就是用继承代替组合。如果经常需要为两个组合类编写极其简单的委托方法,也就是说一个类的行为基本上都委托给了另一个类,则直接让这个类继承另一个类更简便。
为字段增加get/set方法,好处是可以增加校验,或者延迟初始化,而且子类通过覆写方法改变行为,但本类中,尤其是构造函数中是直接访问字段,还是使用get/set方法,是值得讨论的。
如果一个数据必须和其他数据和方法一起使用才有意义,则将该数据项改为类对象。
可以将对象分成两类:reference object(引用对象)和value object(值对象)。值对象有一个非常重要的特征:它们应该是不可变的。要在引用对象和值对象之间做选择有时并不容易,如果希望给一个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所引用此对象,则需要使用引用对象。而在分布式和并发系统中,不可变的值对象则更有用,因为无需考虑它们的同步问题。
如果一个数组的元素各自代表不同的东西,则以对象替换数组,对于数组中的每个元素,以一个字段来表示。
指的是基于MVC模式重构,抽取原来被内嵌在用户界面中的业务逻辑处理,使用户界面和处理业务逻辑的代码分开,然后使用观察者模式实现数据和用户界面的同步。
两个类都需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新2条连接。
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性,则去除不必要的关联。
如果一个字面数值有特别含义,则创建一个常量,根据其意义命名,并将上述的字面数值替换为这个常量。
将类中存在的public字段改为为private,并且提供相应的访问函数。
如果一个方法返回一个集合,则让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
你需要面对传统编程环境中的记录结构,比如一个遗留程序,或者是需要通过一个传统API来与记录结构交流,或是处理从数据库读出的记录,则为该记录创建一个类似外部记录的类,以便日后将某些字段和函数搬移到这个类中。
如果类中有一个数值类型码,但它并不影响类的行为,则以一个新的类替换该数值类型码(感觉用枚举类也不错)。如果一个类型会影响行为,则可以借助多态,以子类取代类型码来处理变化的行为。但如果类型的值在对象的生命周期内会发生变化,或者由于其他原因使得无法用子类,则可以使用状态模式和策略模式来重构。
如果各个子类的唯一差别只在“返回常量数据”的方法上,则在父类中新增一个字段,修改这些方法,使它们返回这个字段并删除子类,这个新增字段可以在对象构造时赋值。
如果某个继承体系同时承担了两项责任,则建立两个继承体系,并通过委托关系让一个可以调用另一个。
将数据记录转化为对象,将大块的行为分解为小块,并移到相关的对象中。
实际指按照MVC模式重构程序
如果某个类做了大量的工作,并且一部分工作是以大量的条件表达式完成的,则建立继承体系,以一个子类表示一种特殊情况。
本文出自 “不积跬步,无以至千里” 博客,谢绝转载!
标签:重构
原文地址:http://wangzhichao.blog.51cto.com/2643325/1705424