在前面的文章中,我们介绍了 《提升编程效率:重构》 以及 《何时开始重构?》。了解了那些能够更好的辅助团队或者个人进行重构,但是要让重构真正产生作用是需要能够代码中的坏味道,并消除代码中的坏味道。
如下图是工作中常见的代码的坏味道:
02.png上图中的坏味道出自《重构》这本书,虽然并不是全部,但是涵盖了日常中最常见的一些代码坏味道。
接触这些坏代码可以分为三类:
-
见名知意的代码坏味道:
-
稍微解释即可掌握的代码坏味道;
-
通过一些例子即可掌握的代码的坏味道;
本文主要聚焦在“见名知意的代码坏味道”,后续两类坏味道将在后续的文章中解释说明:
1. 重复代码
简单的复制/粘贴,或者无意间添加了相同逻辑的代码都是有可能的导致重复代码出现的。
那么为什么重复的代码是一种坏味道?
最明显的就是重复的代码容易造成修改时的遗漏,修改遗漏导致一个问题需要修改多次才能才能确定最终修改完成。如果有一部分修改了,另外一部分没有修改且没有被发现,日后再遇到感觉类似,实则不同的代码会花费大量的时间确定业务上的需求,实现上应该如何处理。
重复代码这类坏味道产生的成本很低,但是带来的影响却是很大。
如何解决重复代码问题?
- Simple Design 为我们提供了参考参考原则:“通过测试,揭示意图,消除重复,最少元素”。
- 如果重复代码发生在一个类中,且两段代码完全重复,可以借助 Extract Method (提炼函数)这个重构手法来消除重复;提炼函数时 IDE 一般都会自动提示是否同时修改重复的代码,减少重构的工作量。
- 如果重复代码发生在一个类中,且两段代码之后部分重复。那么可以将部分重复的代码通过 Extract Method 的手法,提炼到单独的方法中,并替换掉部分重复的代码。
- 如果重复的代码在不同的类中,且这些类是兄弟类,可以使用 Pull Up Method,将重复的代码提炼到父类,并让原本的类继承父类。
- 如果重复的代码在不同的类中,且这些类之间关联性不大,那么可以 Extract Class,将重复的挪动到一个新的类中,原本出现重复的地方来调用这个新产生的类的方法。
- 消除重复之后,检测代码表达的意图是否准确、完成,Extract Method 时可以通过良好的方法名来解释提炼的函数的作用和意图。
2 长函数
顾名思义,长度过长的函数。其中包括两种情况,横向过长,纵向过长。
为什么长函数是一种坏味道?
横向过长时,往往一眼无法快速了解该行代码要表达的意思和中间的过程。当出现 Bug 定位问题时也不容易一次性定位到问题所在。
纵向过长时,往往会感觉某个函数内部逻辑复杂、晦涩难懂。修改代码中也会因为无法照顾到要修改的方法中的其他行代码,而顾此失彼,最终导致难度难修改。经过多次修改后甚至原有的基本结构都会遭到破坏,导致后续修改难度逐渐增加。
如何解决长函数的问题?
- 横向过长的代码,可以通过代码格式化、CheckStyle插件来发现和消除。比如,Lambda 表达式,可以选择在出现第一个“.”时就就开始换行。
List<Node> nodes = items.stream().filter(Item::isFree).filter(Item::notWork).map(Formater::format).filter(Node::hadChildren).filter(Node::hadMarked).collection(Collectors.toList());
这行代码我们需要仔细读 才能清楚中间的过程。采用首个“.”出现换行的将会是如下格式:
List<Node> nodes = items
.stream()
.filter(Item::isFree)
.filter(Item::notWork)
.map(Formater::format)
.filter(Node::hadChildren)
.filter(Node::hadMarked)
.collection(Collectors.toList());
通过对横向代码格式化能够为代码带来更好的可读性。当然你可以在提交代码到仓库时勾选上 commit 时自动格式化代码的选项,避免没有 Check Style 等工具来守护代码,遗漏掉格式问题。
- 纵向过长的代码。往往多个实现细节堆叠在一个方法中造成的,这种情况下使用 Inline Temp(内联局部变量)、 Extract Method 的重构手法来提炼小的函数。一个类中有很多零散的小函数也是常见的,因此提炼函数的同时记住,提炼函数的也是也是考虑创建新的类时候,将不同作用的函数提炼到响应职责的类中。
- 纵向过长的代码,往往存在职责不够单一的情况,保持方法职责的单一有助于维护代码的可读性。通过 2 中 提到的 Extract Method,那么某个具体实现细节可以被提炼到一个小函数中,而原来的函数则职责就编程调度作用。所以方法的单一职责,更清晰的描述应该是一类事情,要么只在处理实现细节,要么处理调度协调代码调用。
public class OrderService{
...
public Order create(OrderDTO orderDTO) {
// 创建条件是否符合 4 行
...
// 货币转换 4 行
...
// 折扣计算 5 行
...
// 将 OrderDTO 转换为 Order 对3行
...
// 存储 Order 1 行
...
// 通知下有业务 5 行
...
return order;
}
}
看遗留系统时和面试作业的时候,总是看到这类代码,可以通过提炼函数并遵守方法的单一职责原则,就能够简单的重构实现一个逻辑更为清晰的代码结构,如下:
public class OrderService {
...
public Order create(OrderDTO orderDTO) {
verify(orderDTO);
Order order = orderRepository.save(orderMapper.toOrder());
notifyService.notify(order);
return order;
}
private void verify(OrderDTO orderDTO) {
// 创建条件是否符合 4 行
...
}
}
public interface OrderMapper {
...
public Order toOrder() {
// 将 OrderDTO 转换为 Order 对3行
Currency currency = CurrentyTranslator.translator(currency); // 货币转换
BigDecimal price = currentyTranslator.calculate(products); // 提炼函数
...
}
}
public class CurrentyTranslator {
public static Currency translate (Currency currency) {
// 货币转换 4 行
...
}
}
public class PriceService {
public BigDecimal calculate(List<Product> products) {
// 折扣计算 5 行
...
return xxx;
}
}
public class NotifyService {
private void notify(Order order){
// 通知下有业务 5 行
...
}
}
上面只是一个简单的重构方法,其中涉及到的重构手法:
Move Field(搬移函数)将上下文相关的变量挪动的一起;
Extract Method (提炼函数) 将某个具体的实现提炼到一个职责单一的方法中。
Extract Method (提炼类)一个类尤其单独的职责,因此将那些和原本的该类的职责关联性不大的逻辑方法提炼到特定的类中。
Inline Field(内联临时变量)如果一个变量对语意理解并没有什么帮助,那么就可以采用内联临时变量的方法,消除显示的定义变量,从而减少代码的行数,同时阅读代码时也会更加清爽、聚焦。
更具实际业务场景还可以借助一些注解、工具类、AOP 来让验证、转换、通知部分变得更加简洁。通过提炼函数的重构手法,能够让后续的重构更加方便可靠。
如果翻阅一些开发规范会发现有的团队规定一个方法不超过 15 行,其实知道这个规范只能获取到一个参考量,注意到行数多对,更重要的时候发现问题后的小步重构。
3 过大的类
顾名思义就是一个类做了太多的事情。SOLID 原则告诉我们类的职责应该是单一的,而一个过大类很可能意味着承担了多个/多类职责。
过大的类为什么是一种坏味道?
由于过大的类承担了过多的职责,很容易导致 重复代码 且 重复代码 不容易被发现,而这往往是坏味道的开始。
如果过大的类对外提供服务发生了变动,并不容易快速响应这样的变化,可以对比一下一个小而职责单一的类中进行修改方便还是在多很多职责。
当过大的类因为某个地方发生变化,很可能导致不相关的调用方的代码也会发生变化,这是一种耦合性的表现。
当过大的类被继承时很可能导致其他的坏味道,例如遗留的馈赠。
因此,保持小而职责单一的类将会对系统的设计有很大的帮助。当然也可以参考 Simple Design,避免过度设计的前提下保持简单的设计。
如何解决过大的类的代码坏味道?
- 观察这个过大的类的属性,看是否有关联的几个属性能够代表一定的业务意思,如果可以使用 Extract Class,将这几个属性挪动到一个新的类中,并将相关操作挪动到新的类中。循环往复,这样一个大的类能够拆分成多个小的且职责较为单一的类。
- 观察这个大类中的方法,看是否存在兄弟关系的方法,如果有可以使用 Extract Subclass (提炼子类)的方法,将相关方法提炼到子类中,并考虑使用继承父类还是面向接口使用 Extract Interface(提炼接口)。这样相似行为的行为聚集在一个类中,拆分到多个类中,并可以进一步和方法的调用发来解耦。
- 进一步观察剩余类的行为,如果这些行为在处理一类事情,那么可以停止了,在处理多类事情,可以按照处理逻辑的类型进一步拆分。
简而言之,使用一个亘古不变的法则:分治法。将过大的类,拆分成多个职责单一的小类,手段是 Extract Class,Extract Subclass,Extract Interface。
4 过长参数列表
当方法的参数列表过长时这也是一种代码的坏味道。
�
为什么参数过长是一种坏味道?
参数过长和过大的类、过长的函数、重复代码一样,起初并不会导致什么错误,但是代码随着时间向前演变过程,会给代码带来很多麻烦。
长参数函数的可读性很差,尤其是存在多个类似长参数方法时,并不容易判断出应该使用哪个方法。
当需要为长参数函数添加新的参数时,将会促使调用方发生变化,且新参数的位置也将让这个方法更加难以理解。
如何解决长参数的代码坏味道?
- 如果传递的几个参数都出自一个对象,那么可以选择使用 Preserve Whole Object(保持完整对象)直接传递该对象。
- 如果方法的参数来自不同的对象,可以选择使用 Introduce Parameter Object(引入参数对象)将多个参数放入一个新的类中,原来方法传递多个分开的参数,现在传递一个包含多个属性的一个对象。
- 如果调用者先计算调用 A 方法得到计算结果,然后将计算结果在传递给这个长参数函数,那么可以考虑去除这个参数,改为在长参数函数中直接调用 A 得到结果,从而消除传递的部分参数,这个重构过程可以参考 Replace Parameter With Method(使函数替换参数)��。
需要的注意的是,有些情况下长参数的存在也是合理的,因为在一定程度上可以避免某些依赖关系的产生。可以通过观察长参数函数变化的频率,并采用“事不过三,三则重构“的原则,保持进行重构的准备。
5 Switch 语句
Switch 语句代表一类语句,比如 if...else, switch... case 语句都是 switch 语句。
为什么 Switch 语句是一种代码坏味道?
首先并不是所有的 Switch 语句都是坏味道,Swith 语句开发中常见的语句。这里带有坏味道的 Switch 语句指的是那些造成重复代码的 Switch语句。例如:根据某个状态来判断执行执行哪个动作。
public Order nextStep(...) {
if (state == 1) {
// do something
} else if (state == 2) {
// do something
} else if (state == 3) {
// do something
} else {
// do something
}
}
这种实现方法很多代码中都会出现,但是多数人使用这种方式添加代码,并不意味着这是一种好的代码。这样的实现方式很容易造成长函数,而且每次修改的位置要非常精准,需要在多个条件中逐个遍历找到最终需要的那个,再修改,可读性上无疑也是很差的。
如何处理 Switch 语句这种代码坏味道呢?**
- 如果 swtich 语句是某个方法的一部分,那么不妨使用 Extract Method(提炼函数)将其先提炼出一个单独的方法,缩小上下文范围。
- 观察多个条件中的动作的关联关系,是否符合多态,如果是将符合多态的几个条件创建对应的类,并使用 Move Method (移动函数)移动到新创建的类中。
- 使用状态模式、枚举等多种实现手段消除其中的 swtich 语句。
如果对有限状态机感兴趣可以参考文章:《Java有限状态机的4种实现对比》
总而言之,一旦打算通过叠加新的 swtich case 来添加新逻辑,那么就应该关注一下代码设计,因为这种操作很有可能就是为后续的代码在挖坑。同时理解清楚那些swtich 语句是具有坏味道的语句。
6 夸夸其谈的未来性
这是工作中最常见的一类问题,比如如果你听到这句话“我将文件上传的实现做了调整 ... 未来再使用的时候将会 ...”就应该警觉起来。
为什么夸夸其谈的未来型是一种代码坏味道?
未来意味着当下并不是必须的,过度的抽象和提升复杂性也会让系统难以理解和维护,同时也容易分散团队的注意力,如果用不到,那么就不值得做。
除非你在进行假设驱动开发,否则代码上总是谈未来容易绑架团队的思想,拿未来不确定的事情来解释事情的合理,会让那些务实者,关注投入产出比的抉择。并且容易让团队进入一个假象。
当业务上变动时,并不能及时的将代码进行变动,因为原来的代码中包含了一种对未来假设的实现,无形中增加了代码的复杂度,而且很容易增加团队沟通成本。
如何解决夸夸其谈的未来性的代码坏味道?
Simple Design (简单设计原则)能够帮助我们作出抉择。当实现业务代码时考虑”通过测试、揭示意图、消除重复、最少元素“。
当发现为未来而写的代码时,可以:
- 删除那些觉的未来有用的参数、代码、方法调用。
- 修正方法名,使方法名揭示当下业务场景的意图,避免抽象的技术描述词。
通过上面两个过程将代码原本的要表达的意思还原回来。
工作中有两类未来性。一类是假设调用方可以怎么使用;一类是未来必然发生的业务功能。代码的坏味道更多的指的是第一种情况,第二种情况可以开发之前体现进行简单设计和拆分,从而避免过度设计,同时可以避免谈未来性,来让代码随着功能一起小步重构并演进。
7 令人迷惑的临时字段
在一些场景下为了在实现上的临时方便性,有的开发者会直接在某个对象上添加一个属性,后续使用在需要的时候使用该属性。
令人迷惑的临时字段的是什么代码坏味道?
一个类包含属性和方法,属性都是该类相关的。而临时向类中添加的字段,虽然临时有关联性,但是单独来看这个类中的属性时,却会让人觉得非常费解。有些接口的返回值就是也是类似原因导致的结果,每次为了方便像类中直接添加一些临时属性,满足了当时的需要,但是后续再使用的时候却并不能区分哪些属性时必须的,哪些是不必须的,以及哪些被添加的字段的上下文分别是什么。
如何解决令人迷惑的临时字段?
- 问题的原因是随意向类上添加字段,解决的方法就是将这个临时字段移走,可以为这个字段找到一个合适的类来存放,也可以使用 Extract Class (提炼类)将这个字段添加到一个新类中,然后将该字段的相关的逻辑移动到该类中,并确定该类的职责。
- 可以将临时字段作为参数进行传递,但是为了避免过长参数的出现,可以选择将临时字段提炼到一个新的类中。
8 过多的注释
这是注释降低代码可读性,甚至误导了代码要要表达的意图。
为什么过多的注释是一种代码坏味道?
首先并不是所有的注视都是坏味道。
如果想通过注释来表达代码的意思,那么代码修改了注释也需要同步进行修改,如果代码修改了但是没有修正这是注释就有可能导致误导。
还有一种注释的坏味道,指的是不使用的代码通过注释掉来表示其弃用。后续代码的阅读者会经常收到断断续续的注释掉的代码影响。降低读代码和改代码的速度。
在 《Clean Code》 中罗列了一些注释的坏味道:
-
喃喃自语
-
多余的注释
-
误导性注释
-
循规方注释
-
日志式注释
-
废话注释
-
用注释来解释变量意思
-
用来标记位置的注释
-
类的归属的注释
-
注释掉的代码
...
如何解决过多的注释的代码坏味道?
造成使用注释的原因很多,可以考虑移除这些注释:
- 删除被注释掉不再使用的代码
- 如果某段代码没有办法轻松的解释清楚,可以使用 Extract Method 来,并使用提炼的方法名来表达意图。
- 删除多余的注释,误导性注释,如有必要可以将方法重命名,解释意图。
- 用来说明变量意思的注释删除掉,对变量进行重命名,如果这个变量并不是必须的可以选择将变量进行 Inline Temp。
上面介绍了代码中常见的 8 中代码坏味道,这些坏味道见名知意,每种坏味道通过简单的几步重构即可解决。面对这些坏味道应该避免延迟解决,随时保持代码的整洁。
网友评论