65.9K
CodeProject 正在变化。 阅读更多。
Home

重构指南

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.68/5 (4投票s)

2018年10月22日

CPOL

8分钟阅读

viewsIcon

6063

重构是软件开发生命周期的一部分。本文粗略概述了重构的主题。

软件生命周期

几乎所有的软件都是在一个美好的计划和崇高的希望下启动的。最初的构想者认为它将满足未来的所有需求。之后,第一个版本实现了,通常它已经不再完全遵循最初的计划。随着时间的推移,新的需求出现了:应该添加新功能,一些行为需要改变,一些bug必须修复。这些活动通常以“尽快完成”的态度进行。不考虑未来,不考虑过去,不考虑架构,只是以最快的方式解决它。过了一段时间,就会出现一个基本上能按预期工作的代码,但代码质量很糟糕,内存管理方式很奇怪,导致了一些棘手的bug。运行时效率也不高。代码存在大量的重复和死代码,要弄清楚在有新需求时应该修改什么变得非常困难。因此,即使是实现一个小型功能也可能花费很长时间。还有一点很重要:大多数开发人员都讨厌处理这种低质量的代码!

重构的含义是什么?

重构是指以提高代码质量的方式修改代码,但其行为保持不变。因此,例如修复bug或使行为对用户更友好不属于重构的范畴。但是,提高代码可读性或模块化的代码更改,或者实现更好运行时性能或内存使用的更改,则属于重构的范畴。

重构的好处是什么?

在重构过程中,您总是在提高代码质量,换句话说:您正在改进您的代码。代码质量是一个相当复杂的话题,但我认为它主要围绕以下几点:稳定性、可读性、模块化、可维护性、可靠性、效率、安全性、可测试性和大小。因此,在重构过程中,您会针对代码的一个或多个方面进行改进,并根据选定的目的使代码更好。请注意,不同的目的可能相互矛盾。例如,通过使用更多内存来达到更好的运行时性能是很常见的。

这是理论基础。但现在让我们看看实践。正如我在引言中所说,软件的代码库通常会随着时间的推移变得越来越复杂,同时其质量也越来越差。通常,它会变得可读性更差,可维护性更差,后期可靠性也更差。这意味着到处都是bug。由于可读性差,找到bug的根本原因需要很长时间;由于可维护性差,修复这些问题也需要很长时间。由于代码结构不当,实现新功能也需要很长时间。因此,项目将进入一个阶段,很多人都在忙碌,但却没有取得多大成果。看起来他们什么也没做,只是让代码活着。这是一种时间和金钱的浪费。这个阶段通常是重构代码库以达到更好的可读性、可维护性和稳定性的时间。所有其他方面都取决于具体情况,例如,如果程序运行速度太慢,那么运行时效率当然也是一个重点。

低层次的好处是更好的代码质量,高层次的好处就是金钱。

如何开始重构?

正如我所说,重构意味着在不改变行为的情况下修改代码。在修改任何东西之前,您需要非常清楚地了解当前的行为。这应该以某种方式进行文档化。最简单的解决方案是将其写在文档中。但更有效的解决方案是为您的代码实现自动化测试,如果可能的话。事实上,除了极少数特殊情况外,几乎所有情况都可能实现。您需要实现能够覆盖全部功能的测试用例,这些测试用例独立于实现细节,并且在当前代码库上能够通过。每次更改后,您可以运行它们来检查它们是否仍然是“绿色”(即通过)。

一旦完成,最好深入到组件级别。在这里,即使它们尚不存在,您也应该从组件的角度进行思考:您的程序有哪些主要功能,如何对这些功能进行分类,您的架构有哪些不同的层次。所有这些都需要考虑。组件应该有清晰的职责和明确的输入输出接口。

一旦您确定了主要组件,就尝试将现有代码库拆分到这些组件中,并使用它们之间的预定义接口。这可能具有挑战性。有时您可能需要拆分类或函数。始终确保您的代码可以编译并且自动化测试通过。

如果您的代码已经按照组件进行组织,那么您可以使用它们的接口编写一些组件级别的自动化测试。这样,以后如果测试失败,您就能知道哪个组件是问题的根源。

什么应该被重构?

重构可以在不同级别和不同目的下进行。让我们从主要目标是提高可读性和可维护性的情况开始。

我建议从最高级别开始。检查您当前的组件。它们是否具有清晰的单一职责?它们的接口是否足够清晰?是否存在重复的接口?通信流程是否尽可能简单?如果任何一个问题的答案是否定的,您应该从这个级别开始。将过于复杂的组件拆分成多个,删除不需要的接口,使组件之间的通信更容易。

接下来,检查每个组件及其类:它们是否具有清晰简单的职责?它们是否有清晰的公共接口?它们之间的连接是否最优?如果任何一个答案是否定的,您应该在这个级别进行更改。在这个级别,您应该始终遵循所谓的 SOLID 原则

  • 单一职责原则
    您的每个类都应该有一个简单清晰的职责

  • 开闭原则
    您的代码应该对扩展开放,对修改关闭

  • 里氏替换原则
    这可能有点复杂。但它意味着,您的任何父类都可以被它的任何子类替换,而不会破坏功能

  • 接口隔离原则
    您的接口应该小巧清晰,并且有一个明确定义的用途。因此,您应该避免使用返回大量数据的接口。

  • 依赖倒置原则
    您应该以这样的方式设计您的类,使得您的类的依赖项可以通过某些 setter 函数或构造函数参数进行设置。这样,您以后就可以将它们更改为任何子类型。例如,如果您有一个用于创建日志文件的 Logger 类,并且可以通过 setter 函数进行更改,那么您可以在 XML Logger、JSON Logger 或 Simple Logger 之间切换您的 Logger,前提是这些类都派生自同一个 Logger 基类。当您需要模拟依赖项进行单元测试时,这也很有帮助。

完成类的重构后,进入函数级别。在此步骤中,尝试消除参数过多的函数。尝试将长函数拆分成多个。尝试消除代码重复。明确区分返回某个值但不改变对象状态的函数,以及用于改变对象状态的函数。此时,请确保您的代码已包含单元测试。如果没有,请进行覆盖。确保您的函数名和变量名足够清晰,如果需要则重命名。定义常量而不是一些魔术数字。这是一个非常复杂的话题,无法在一篇文章中全部解决。现在,完成此级别的重构后,退回到类级别。现在是否有任何类应该被拆分成多个?如果有,就这样做。下一步,在组件级别执行相同的操作。此时,您可能会发现这是一个永无止境的故事。没有完美的代码。只有更糟的代码和更好的代码。您的代码可以始终更好,但永远不可能完美。您需要找到重构和实现新功能之间的平衡。

如果此次重构的目标是达到更好的性能,您需要从另一个角度分析您的代码。您应该监控您的运行时,并识别从运行时角度来看最关键的组件和函数。找到它们之后,您可以做两件事:寻找优化其算法和通信方式的方法。或者将某些部分更改为多线程。多线程再次是一个复杂的主题,请小心处理,特别注意共享内存字段。还有一点:创建新线程并不总是能提高性能!

 

摘要

如前所述,重构是一个非常复杂的主题,但几乎在所有地方都需要它。我试图为您提供一个粗略的概述,希望它对您有所帮助。

© . All rights reserved.