重构 02:开始诊疗
Translated from:Refactoring
大多数代码,都具备“屎山”的特征。
代码的味道
- 纳尼?代码怎么会有味道?
- 虽然闻不出来…但是我敢打赌它肯定会发臭!
诊断:臃肿
臃肿的代码,包括臃肿的函数和类等等,太过臃肿的代码导致程序几乎无法运行。通常臃肿的问题不会马上就出行,而是随着时间推移,随着程序的发展,而逐渐积累(特别是没有人去关心这个问题时)。
病症:过长的函数
症状
一个函数包含太多的代码行。一般来说,任何超过十行的函数都应该让你产生不安的感觉。
产生原因
就像 Hotel California (译者注:不是很清楚这个梗,应该是这个),某些函数总是在一直添加东西,但没有任何逻辑被拆分或删除。由于写代码比读代码容易,这种“臭味”一直没有被注意到,直到函数变成一个丑陋、巨大的怪兽。
从开发人员的心态来说,创建一个新函数往往比在原有函数中添加逻辑更难。“只是再加两行代码而已,没有必要给这段逻辑单独放一个函数里…”。带着这种想法,我们经常在函数中塞入一行又一行的代码,最终产生了一团乱七八糟,像意大利面条一样的东西。
解决方案
一个经验法则是,如果你觉得需要在一个函数里,针对一段代码需要写注释,那你应该把这段代码放到一个新的函数中。如果是一定需要写注释的代码,哪怕是一行,也应该拆分出一个单独的函数。如果函数有一个描述性的名字,那后面的开发人员就不需要看代码来了解内部的运作。
- 要减少函数体的长度,可以参见拆分函数
- 如果局部变量和参数干扰了函数拆分,那试试使用查询替代临时计算、引入对象形参和对象整存整取
- 如果前面的函数都没有用,请尝试通过替换函数为函数对象将整个函数移到一个单独的对象中。
- 条件运算符和循环是一个很好的标识,通过这些可以将不同部分的代码迁移到单独的函数中。对于条件运算,可以使用条件分解法;对于循环,可以使用拆分函数
小结
- 在所有面向对象的项目中,函数越短的类寿命最长。一个函数或者函数越长就越难理解和维护。
- 此外,长函数为不必要的重复代码提供的完美的藏身之处。
优化后会影响性能吗?
很多人说,增加太多函数会影响性能?但是在几乎所有试验中,其带来的性能损耗微乎其微,不值得担心。
而且通过以上措施,我们获得了清晰易懂的代码,在优化过程中,更有可能找到真正有效的函数来重组代码,获得真正的性能提升。
病症:过长的类
症状
一个类包含太多字段/函数/代码行。
产生原因
一般来说,类一开始被创建的时候都会比较小,但是一旦随着岁月流逝,这个类就会变得越来越大,功能也会越来越复杂。
和过长的函数一样,比起新创建一个类,程序员们更喜欢将新功能放在现有的类中。
解决方案
当一个类包含了大量的函数时,需要进行分析并且通过以下手段尝试将它们拆分。
- 如果类中包含了可以独立的逻辑或者组件时,可以直接 拆分类
- 如果类中包含的逻辑可以差异化实现,或者很少运行到的逻辑,可以尝试 拆分子类
- 如果类中的函数和逻辑可以完全抽象,调用时再进行实现,可以尝试 拆分接口
- 对于复杂的UI界面,比较好的函数是将模型数据和逻辑拆分到单独的对象中。这是可能会在2个地方存储一些数据的副本,并且保持数据的一致性。重复数据观察模式 提供了一种处理这种情况的方案。
重构了之后…
- 重构这些大型类后,可以显著降低开发人员的心智负担。
- 通常拆分大类后,可以避免很多重复的代码和功能。
病症:基础类型的滥用
症状
- 一些简单的事物(如货币、范围、电话号码特殊字符等)使用基础类型而不是使用对象
- 使用常量来编码信息,比如定义常量 USER_ADMIN_ROLE = 1来描述具备管理员权限的用户
- 使用字符串常量作为数据类数组的字段名
产生原因
和其他问题一样,基础数据的滥用也是一时的懈怠。“只是想要一个东西放临时的数据!”程序员产生这种想法的时候,都是觉得比起创建一个全新的类,直接创建一个基础类型更方便。当每一次遇到相同的情况时,都会有这样偷懒的想法,导致我们最后创建了一个又一个的基础类型存放数据,而容纳这些基础类型属性的类也逐渐变得越来越大。
基础类型通常用于“仿冒”类型。比如,可以使用一组数字或字符串(而不是单独的数据类型)来构成某些实体的允许值列表。然后,通过常量为这些特定的数字和字符串提供易于理解的名称,这就是为什么它们被滥用的原因。
另外一个不正确使用基础类型的方式是使用基础类型“仿冒”类的字段。比如类中包含了大量不同的数据数组,在类中指定的字符串常量用于获取这些数据数组的索引。
解决方案
- 如果代码中存在大量的基础类型,一般可以把这些代码写在专门的类里,甚至与这些代码相关的行为也可以转移到这个类中。关于这个解决方案可以参见 使用对象替换零散数据。
- 如果函数参数中使用了基础类型,可以考虑 引入对象参数 或者 保留整个对象。
- 当发现变量中存在很复杂的数据时,可以考虑 使用类替换类型编码,使用子类替换类型编码,使用状态/策略替换类型编码。
- 如果变量中存在数组,可以考虑 使用对象替换数组。
重构了之后…
- 使用对象替换基础类型,可以使代码更加灵活。
- 重构以后代码更容易理解,更加具备组织性。对特定数据的操作都在同一个地方而不是分散在各处。在阅读代码时不会出现难以猜测的奇怪常量,或者为什么有奇怪的常量出现在数组中。
- 更容易找到重复的代码。
病症:大串的函数形参
症状
一个函数的参数超过三个或四个。
产生原因
把几种类型算法合并到一个函数中后,可能会给这个函数带来一长串的形参。大串的形参可能是为了控制函数中哪些逻辑被执行、被如何执行。
大串的形参也可能是拆分类的副产品。例如原始函数中创建特定对象的代码转移给了调用方(译者注:这样原始函数就会更加的灵活),调用方创建对象后通过参数传递,这样这个特定对象就和原始函数解耦了。但是如果调用方传递了多个不同类型的对象,每个对象都需要自己特有的参数,这样原始函数可能就需要大串的形参了。
函数中大串的形参会导致理解困难,当形参越来越长,函数也就越来越难以使用。通常也可以使用相关对象来代替大串形参,如果这个对象不能包含所有需要的信息,通过会创建额外的对象再作为函数参数。
解决方案
- 检查传递参数的值。如果有些参数只是另一个对象的调用结果,那么可以 使用函数调用替换参数。这个对象可以放在自己的类字段中,也可以作为函数参数传递。
- 不要将从另一个对象接受到的一组数据直接作为参数传递,而是将对象本身传递给函数,参见 保留整个对象。
- 如果有几个不相干的数据元素,有时可以 引入对象参数 ,将他们合并为一个参数对象。
重构了之后…
- 精简、易读的代码。
- 可能会发现以前没注意到的重复代码。
病症:重复代码簇
特征
有时相同的代码簇出现在程序的不同部分,比如连接数据库的参数。这些代码簇应该被整理成类。
产生原因
通常这些重复代码是由于架构不完善或者“Ctrl-C + Ctrl-V编程”造成的。
如果要确认一些代码是不是重复代码簇,只要删除其中一个数据值,看看其他值是否还有意义。如果不是这种情况,则表明这组变量应合并到一个对象中,这是一个好兆头。
解决方案
- 如果重复的数据由一个类的字段组成,使用 拆分类 将字段移到它们自己的类中。
- 如果在函数的参数中传递了相同的数据块,使用 引入对象参数 将它们设置为一个类。
- 如果只是部分数据被传递给其他函数,请考虑将整个数据对象传递给函数,而不仅仅是单个字段。保留整个对象 将帮助解决这个问题。
- 检查使用这些代码的字段,有时将这些代码转移到数据类中可能是个好主意。
重构之后…
- 代码更有组织性易于理解。对特定数据的操作现在被收集在一个地方,而不是杂乱无章地分布在整个项目中。
- 减少代码体积。
特例:什么时候忽略这个问题?
在函数参数中传递整个对象,而不是只传递它的值(基础类型),可能会在两个类之间产生意料之外的依赖关系。