RefactoringGuru 代码异味和重构技巧总结
整理自 RefactoringGuru
代码异味
——什么?代码如何“闻味道”??
——它没有鼻子...但它肯定会发臭!
代码膨胀
【代码膨胀】是代码、方法和类,它们的规模已经增加到了难以处理的地步。通常,这些异味不会立即出现,而是随着程序的演化而积累(尤其是当没有人努力根除它们的时候)。
过长方法
方法包含的代码行太多。一般来说,任何超过十行的方法都会让你产生疑问。
过大的类
一个类包含许多字段/方法/代码行。
基本类型偏执
在简单任务中使用基本类型而不是小对象(例如货币、范围、电话号码的特殊字符串等)
使用常量来编码信息(例如常量USER_ADMIN_ROLE=1
表示具有管理员权限的用户。)
在数据数组中使用字符串常量作为字段名。
过长参数列表
一个方法有三个或四个以上的参数。
数据泥团
有时,代码的不同部分包含相同的变量组(例如用于连接数据库的参数)。这些组应该转化为它们自己的类。
面向对象滥用
所有这些异味都是面向对象编程原理的不完整或不正确应用。
switch
语句
你有一个复杂的switch
运算符或if
语句序列。
临时字段
临时字段仅在特定情况下获取其值(因此对象需要它)。除此之外,它们是空的。
被拒绝的继承
如果子类只使用从其父类继承的一些方法和属性,那么层次结构就不正常。不需要的方法可能只是不使用,或者被重新定义并发出异常。
具有不同接口的备选类
两个类具有相同的函数,但方法名不同。
更改的阻碍
这些异味意味着,如果你需要在代码的某个地方更改某些内容,那么你也必须在其他地方进行许多更改。因此,程序开发变得更加复杂和昂贵。
发散式更改
在更改类的时候,你发现自己必须更改许多不相关的方法。例如,添加新产品类型时,必须更改查找、展示和订购产品的方法。
散弹式更改
修改任何东西都需要对许多不同的类做出许多小的更改。
平行继承体系
每当你为一个类创建一个子类时,你就会发现自己需要为另一个类创建一个子类。
可有可无的东西
可有可无的东西是毫无意义和不必要的,如果没有它,代码就会更干净、更高效、更容易理解。
注释
方法中充满了解释性注释。
重复代码
两段代码看起来几乎相同。
冗余类
理解和维护类总是需要花费时间和金钱。因此,如果一个类不足以吸引你的注意力,它应该被删除。
数据类
数据类是指只包含字段和用于访问字段的方法(获取器和设置器)的类。这些只是其他类使用的数据容器。这些类不包含任何附加功能,并且不能独立操作它们所拥有的数据。
死代码
变量、参数、字段、方法或类已不再使用(通常是因为它已过时)。
夸大通用性
存在未使用的类、方法、字段或参数。
耦合器
这一组中的所有异味都会导致类之间的过度耦合,或者显示如果耦合被过度委托所取代会发生什么。
功能依赖
一个方法访问另一个对象的数据多于它自己的数据。
过度亲密
一个类使用另一个类的内部字段和方法。
消息链
在代码中可以看到一系列类似于$a->b()->c()->d()
的调用。
中间人
如果一个类只执行一个操作,将工作委托给另一个类,那么它为什么存在呢?
其他异味
不完善的库类
库迟早会停止满足用户需求。由于库是只读的,所以问题的唯一解决方案,也就是更改库,通常是不可能的。
重构技巧
组合方法
很多重构都致力于正确地组合方法。在大多数情况下,过长的方法是万恶之源。这些方法中变幻莫测的代码隐藏了执行逻辑,使得该方法极难理解,甚至更难更改。
这一组中的重构技巧简化了方法,消除了代码重复,并为未来的改进铺平了道路。
提取方法
问题:你有一个可以组合在一起的代码片段。
解决方案:将此代码移动到一个单独的新方法(或函数),并用对该方法的调用替换旧代码。
内联函数
问题:当方法主体比方法本身更明显时,请使用此技巧。
解决方案:用方法的内容替换对方法的调用,并删除方法本身。
提取变量
问题:你的表达式很难理解。
解决方案:将表达式或其部分的结果放在独立的变量中,这些变量是自解释的。
内联临时变量
问题:你有一个临时变量,它被分配了一个简单表达式的结果,仅此而已。
解决方案:用表达式本身替换对变量的引用。
用查询替换临时变量
问题:将表达式的结果放在局部变量中,以便以后在代码中使用。
解决方案:将整个表达式移动到一个单独的方法,并从中返回结果。查询方法,而不是使用变量。如有必要,在其他方法中加入新方法。
拆分临时变量
问题:你有一个局部变量,用于在方法中存储各种中间值(循环变量除外)。
解决方案:对不同的值使用不同的变量。每个变量应该只负责一个特定的事情。
移除参数赋值
问题:某些值被赋给了方法体中的参数。
解决方案:使用局部变量而不是参数。
用方法对象替换方法
问题:你有一个很长的方法,其中局部变量相互交织,以至于你不能应用【提取方法】。
解决方案:将该方法转换为一个单独的类,以便局部变量成为该类的字段。然后可以将该方法拆分为同一类中的多个方法。
替代算法
问题:所以你想用一个新的算法替换现有的算法?
解决方案:用新算法替换实现算法的方法体。
在对象间移动功能
即使你在不同的类之间,以不太完美的方式分布了功能,仍然存在希望。
这些重构技术展示了如何在类之间安全地移动功能,创建新的类,以及隐藏实现细节以防公开访问。
移动方法
问题:一个方法在另一个类中使用的次数多于在它自己的类中使用的次数。
解决方案:在使用该方法最多的类中创建一个新方法,然后将代码从旧方法移动到这里。将旧方法的代码转换为对另一个类中新方法的引用,或者将其完全删除。
移动字段
问题:一个字段在另一个类中使用的次数比在它自己的类中使用的次数多。
解决方案:在新类中创建一个字段,并将旧字段的所有使用重定向到该字段。
提取类
问题:当一个类做两个类的工作时,会非常笨拙。
解决方案:相反,创建一个新类,并将负责相关功能的字段和方法放在其中。
内联类
问题:一个类几乎什么都不做,也不负责任何事情,也没有为它规划额外的责任。
解决方案:将所有功能从该类移动到另一个类。
隐藏委托关系
问题:客户端从对象 A 的字段或方法中获取对象 B。然后客户端调用对象 B 的方法。
解决方案:在类 A 中创建一个新方法,将调用委托给对象 B。现在客户端不知道也不依赖于类 B。
移除中间人
问题:一个类有太多的方法,这些方法只是委托给其他对象。
解决方案:删除这些方法,并强制客户端直接调用最终方法。
引入外部方法
问题:实用程序类不包含所需的方法,并且无法将该方法添加到该类中。
解决方案:将该方法添加到客户端类,并将实用程序类的对象作为参数传递给它。
引入本地扩展
问题:实用程序类不包含你需要的某些方法。但不能将这些方法添加到类中。
解决方案:创建一个包含这些方法的新类,并使其成为实用程序类的子类或包装器。
组织数据
这些重构技术有助于数据处理,用丰富的类功能替换基本类型。
另一个重要的结果是解开了类的关联,这使得类更具可移植性和可重用性。
自封装字段
问题:你直接访问类内的私有字段。
解决方案:为字段创建一个获取器和设置器,并仅使用它们访问字段。
用对象替换数据值
问题:一个类(或一组类)包含一个数据字段。该字段有自己的行为和相关数据。
解决方案:创建一个新类,将旧字段及其行为放在该类中,并将该类的对象存储在原始类中。
将值更改为引用
问题:所以你有单个类的许多相同实例,并需要用单个对象替换它。
解决方案:将相同的对象转换为单个引用对象。
将引用更改为值
问题:你有一个太小且很少更改的引用对象,因此无法管理其生命周期。
解决方案:将其转化为值对象。
用对象替换数组
问题:你有一个包含各种类型数据的数组。
解决方案:将数组替换为每个元素都有单独字段的对象。
重复的被观测数据
问题:存储在类中的领域数据是否负责GUI?
解决方案:那么最好将数据分成不同的类,确保领域类和GUI之间的连接和同步。
将单向关联改为双向关联
问题:你有两个类,每个类都需要使用另一个类的功能,但它们之间的关联只是单向的。
解决方案:将缺少的关联添加到需要它的类中。
将双向关联改为单向关联
问题:类之间存在双向关联,但其中一个类不使用另一个类的功能。
解决方案:删除未使用的关联。
用符号常量替换幻数
问题:你的代码使用了一个具有特定含义的数字。
解决方案:将这个数字替换为一个常量,该常量有一个人类可读的名称来解释数字的含义。
封装字段
问题:你有一个公共字段。
解决方案:将字段设置为私有,并为其创建访问方法。
封装集合
问题:一个类包含一个集合字段和一个用于处理集合的简单获取器和设置器。
解决方案:将获取器的返回值设为只读,并创建用于添加/删除集合元素的方法。
用类替换类型代码
问题:一个类有一个包含类型代码的字段。这种类型的值不用于运算符条件,也不会影响程序的行为。
解决方案:创建一个新类,并使用其对象而不是类型代码的值。
用子类替换类型代码
问题:你有一个直接影响程序行为的代码类型(此字段的值触发条件中的各种代码)。
解决方案:为代码类型的每个值创建子类。然后将相关行为从原始类提取到这些子类中。用多态替换控制流代码。
用状态/策略替换类型代码
问题:你有一个影响行为的代码类型,但不能使用子类来消除它。
解决方案:用状态对象替换类型代码。如果需要用类型代码替换字段值,则另一个状态对象为“已插入”。
用字段替换子类
问题:你的子类只在(常量返回)方法上有所不同。
解决方案:用父类中的字段替换方法,并删除子类。
简化条件表达式
随着时间的推移,条件的逻辑往往变得越来越复杂,还有更多的技术可以解决这个问题。
分解条件
问题:你有一个复杂的条件(if-then/else
或switch
)。
解决方案:将条件的复杂部分分解为单独的方法:条件、then
和else
。
合并条件表达式
问题:你有多个条件产生相同的结果或操作。
解决方案:将所有这些条件合并到一个表达式中。
合并重复的条件片段
问题:在条件语句的所有分支中都可以找到相同的代码。
解决方案:将代码移到条件之外。
移除控制标志
问题:有一个布尔变量充当多个布尔表达式的控制标志。
解决方案:使用break
、continue
和return
代替变量。
使用守卫子句来代替嵌套的条件判断
问题:有一组嵌套的条件,很难确定代码执行的正常流程。
解决方案:将所有特殊检查和边界情况隔离到单独的子句中,并将其放在主要检查之前。理想情况下,你应该有一个条件列表,一个接一个。
用多态替换条件
问题:你有一个条件,根据对象类型或属性执行各种操作。
解决方案:创建与条件的分支相匹配的子类。在它们中,创建一个共享方法,并将代码从条件的相应分支移动到它。然后用相关的方法调用替换条件。结果是,根据对象类,可以通过多态实现正确的实现。
引入空对象
问题:由于一些方法返回null
而不是真实对象,所以在代码中有很多null
检查。
解决方案:返回一个显示默认行为的空对象,而不是null
。
引入断言
问题:要使部分代码正常工作,某些条件或值必须为true
。
解决方案:用特定的断言检查替换这些假设。
简化方法调用
这些技术使方法调用更简单、更容易理解。这反过来简化了用于类之间交互的接口。
重命名方法
问题:方法的名称不能解释该方法的功能。
解决方案:重命名该方法。
添加参数
问题:方法没有足够的数据来执行某些操作。
解决方案:创建一个新参数来传递必要的数据。
删除参数
问题:方法体中没有使用某个参数。
解决方案:删除未使用的参数。
将查询与修改分开
问题:是否有一个方法可以返回一个值,但也可以更改对象内部的某些内容?
解决方案:将该方法分为两种不同的方法。正如你所料,其中一个应该返回值,另一个则修改对象。
将方法参数化
问题:多个方法执行类似的操作,这些操作只在其内部值、数字或操作上有所不同。
解决方案:通过使用一个将传递必要特殊值的参数来组合这些方法。
用显式方法替换参数
问题:一个方法被分成几个部分,每个部分的运行取决于一个参数的值。
解决方案:将方法的各个部分提取到它们自己的方法中,并调用它们,而不是原始方法。
保存整个对象
问题:从一个对象中获取多个值,然后将它们作为参数传递给一个方法。
解决方案:相反,尝试传递整个对象。
用方法调用替换参数
问题:调用一个查询方法并将其结果作为参数传递给另一个方法,而该方法可以直接调用该查询。
解决方案:不要通过参数传递值,而是尝试在方法体中放置一个查询调用。
引入参数对象
问题:你的方法包含一组重复的参数。
解决方案:用对象替换这些参数。
移除设置方法
问题:字段的值应该只在创建时设置,之后任何时候都不能更改。
解决方案:删除设置字段值的方法。
隐藏方法
问题:一个方法不被其他类使用,或者只在它自己的类层次结构中使用。
解决方案:将方法设置为私有或受保护。
用工厂方法代替构造器
问题:你有一个复杂的构造器,它的功能不仅仅是在对象字段中设置参数值。
解决方案:创建一个工厂方法并使用它替换构造器调用。
用异常替换错误代码
问题:方法返回指示错误的特殊值?
解决方案:抛出一个异常。
用测试替换异常
问题:在一个简单的测试就能完成任务的地方抛出异常?
解决方案:用条件测试替换异常。
处理泛化
抽象有自己的一组重构技术,主要关于沿着类继承层次结构移动功能、创建新的类和接口、用委托代替继承以及相反。
上移字段
问题:两个类具有相同的字段。
解决方案:从子类中删除字段,并将其移动到超类。
上移方法
问题:你的子类具有执行类似工作的方法。
解决方案:使方法相同,然后将它们移动到相关的超类。
上移构造器主体
问题:你的子类的构造器的代码基本相同。
解决方案:创建一个超类构造器,并将子类中相同的代码移动到它。在子类构造器中调用超类构造器。
下移方法
问题:超类中实现的行为是仅由一个(或几个)子类使用的吗?
解决方案:将此行为移动到子类。
下移字段
问题:字段是否仅用于少数子类?
解决方案:将字段移动到这些子类。
提取子类
问题:某个类具有仅在某些情况下使用的功能。
解决方案:创建一个子类,并在这些情况下使用它。
提取超类
问题:有两个类具有相同的字段和方法。
解决方案:为它们创建一个共享超类,并将所有相同的字段和方法移动到其中。
提取接口
问题:多个客户端使用类接口的同一部分。另一种情况:两个类中的部分接口是相同的。
解决方案:将这个相同的部分移动到它自己的接口。
折叠层次结构
问题:你有一个类层次结构,其中一个子类实际上与其超类相同。
解决方案:合并子类和超类。
形成模板方法
问题:你的子类实现的算法包含顺序相同的类似步骤。
解决方案:将算法结构和相同的步骤移动到一个超类,并将不同步骤的实现留在子类中。
用委托替换继承
问题:有一个子类只使用其超类的一部分方法(或者不可能继承超类数据)。
解决方案:创建一个字段并在其中放置一个超类对象,将方法委托给超类对象,并摆脱继承。
用继承替换委托
问题:一个类包含许多简单的方法,这些方法将委托给另一个类的所有方法。
解决方案:使该类继承另一个类,这样就不需要委托方法。