为什么软件开发需要重构?

经验上,一开始的设计总是会有问题的。
关注者
336
被浏览
80,453
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

正好最近在整理跟重构理论及发展相关的材料。

先上结论:其实软件开发并不一定需要重构

作为《

重构

》这本书的译者,我有一个观点:重构已死。当然这种“xx已死”的调调一听就知道是哗众取宠的。更加严格的说法应该是:《重构》书中所描述的、针对面向对象语言单一代码库的重构技术,是在一个独特历史背景下发展成型的产物;在当今的软件开发历史背景下,这种技术的适用范围正在变窄,且适用的问题域和解决方案都已相当成熟,因此不再有继续讨论的价值

=== 第一更:重构是(和不是)什么 (2016-01-02) ===

为了使接下来的讨论有一个可靠的基础,首先需要对“重构”(refactoring)这个词作一个明确的定义。按照《重构》书中的定义:

第一个定义是名词形式:

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

“重构”的另一个用法是动词形式:

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

上述定义中,第一个值得注意的关键词是“不改变软件可观察行为”。按照这个关键词,

@冯东

所说的情景就不是重构(当然,这还取决于他说的“做错”和“修正”如何定义,我假设“修正一个错误”意味着“改变软件可观察的行为”)。

第二个关键词是“重构手法”。一次重构之所以能成为一次重构,它必须遵循一定的重构手法,而不是随便调整。很多人在阅读这本书的时候只看前四章,真正动手改代码的时候根本不遵循重构手法,这样的行为也不是重构。我在这里采用一个较为严格的限定:只有遵循已经发表的重构手法时,才能认为是在进行重构。

第三个关键词是“软件内部结构”。这实际上是非常有玄机的一个词。玄机我们暂且按下不表,从

Opdyke的论文

到《重构》再到《

修改代码的艺术

》,重构理论的早期奠基作品谈论的都是单进程、单代码库的代码重构。因此,我倾向于将“软件内部”限定为“单进程、单代码库内”。

现在,当我讨论“软件开发是否需要重构”时,我讨论的是:

软件开发是否需要:

  1. 不改变软件可观察行为的前提下,
  2. 采用已经发表的重构手法
  3. 单进程、单代码库内的软件结构进行调整。

而我的观点是:在现代软件开发的环境下,越来越不需要。

=== 第二更:为什么重构 (2016-01-06) ===

以下内容出自《重构》第2.2节。我用粗体标注出了一些关键词,这些关键词将有助于我们理解重构技术背后的假设。

重构改进软件设计

如果没有重构,程序的设计会逐渐腐败变质。当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员愈来愈难通过阅读源码而理解原本设计。重构很像是在整理代码,你所做的就是让所有东西回到应该的位置上。代码结构的流失是累积性的。愈难看出代码所代表的设计意涵,就愈难保护其中设计,于是该设计就腐败得愈快。经常性的重构可以帮助代码维持自己该有的形态。


重构使软件更容易理解

所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。你得及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就是“准确说出我所要的”。除了计算机外,你的源码还有其他读者:几个月之后可能会有另一位程序员尝试读懂你的代码并做一些修改。我们很容易忘记这第二位读者,但他才是最重要的。计算机是否多花了几个小时来编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才关系重大—如果他理解你的代码,这个修改原本只需一小时。


重构帮助找到bug

对代码的理解,可以帮助我找到bug。我承认我不太擅长调试。有些人只要盯着一大段代码就可以找出里面的bug,我可不行。但我发现,如果对代码进行重构,我就可以深入理解代码的作为,并恰到好处地把新的理解反馈回去。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是想不把bug揪出来都难。


重构提高编程速度

我强烈相信:良好的设计是快速开发的根本──事实上,拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。修改时间愈来愈长,因为你必须花愈来愈多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁,新特性需要更多代码才能实现。真是个恶性循环。

从这几个重构的原因中,我们看到了下列几个(我在引文中加粗的)关键词,我们可以来分析一下它们背后隐含的假设:

  • 短期目的 => 编程时仅考虑短期目的是不好的,也就是说,代码库有比较长的生命周期。
  • 另一位程序员 => 在这个代码库上工作的人数会比较多,且人员的变动比较频繁。
  • 一大段代码 => 代码库的规模比较大。
  • 添加新功能 => 一个代码库承担多种责任。

所以从2.2节中我们不仅可以读到为什么重构,而且可以读出作者没有明言的假设:作者在谈论的是规模较大、生命周期较长、承担了较多责任、有一个较大(且较不稳定)团队在其上工作的单一代码库。当然所有这些都是相对度量,例如多少行代码算大、生存多长时间算长,没有绝对而清晰的界定。

那么接下来我们就要问:为什么重构技术有这样一些假设?这些假设是在什么样的时代背景下形成的?这些假设在今天的时代背景下是否仍然适用?如果这些假设不再适用,重构技术是否仍然适用?

=== 第三更:重构的时代背景 (2016-01-07) ===

可能令人吃惊:重构出现的时间相当早,比Java早,比设计模式早。

William Opdyke

的博士论文

Refactoring Object-Oriented Frameworks

下载PDF

)发表于1992年,这篇论文是重构理论的奠基之作。(BTW,Opdyke这位老兄相当牛逼,他的博士导师是GoF之一的

Ralph Johnson

。)那么在Java都还没出现之前,重构的早期玩家们玩的是什么语言呢?答案是“Smalltalk”,传说中的“

Ruby之根

”——好吧,跑题了,打住。《重构》的出版是1999年,所以基本上我们可以认为,重构是上世纪90年代浮现、并在本世纪头十年引起重视的技术。

于是我们要问了:为什么在世纪之交的这十多年里,规模大、承担责任多的单一代码库成为了主流?在一个代码库、一个进程里承载一个系统所有的功能,这并不是一个古而有之的做法。例如在一个典型的基于

VxWorks

的电信系统中,职责划分不是以函数、而是以进程为单位。即便在Sun推荐的J2EE架构(

docs.oracle.com/cd/E196

)中,业务是分散在若干个EJB中的,每个EJB可以(尽管未必总是会)被部署为一个独立的进程,所以整个系统也不必(尽管仍然可以)存在于单一代码库中。

然而Sun推荐的J2EE架构并没有被广泛接受,反倒是以Spring为代表的“轻量级J2EE”最终成为了主流。我在《

不敢止步

》里讲到,“轻量级J2EE”的核心架构思想实际上就是Martin Fowler的

分布式对象设计第一法则

分布式对象设计第一法则:不要分布你的对象!

另一种风格则是Apache、Jon Tirsen、Rod Johnson 等开源先锋推荐的Martin Fowler 在PoEAA 里总结的:不要分布对象,在一个Java 进程里完成所有业务逻辑, 用集群解决单台服务器负载过重的问题。这种架构风格,再加上后来的“Shared Nothing Architecture”,实际上就把Web 应用简化成了一个单进程编程的问题: 不是“使远程调用变得透明”,而是根本没有远程调用。

这个架构风格的最大价值是让开发者可以在自己的桌面电脑上复制整个生产环境,从而能够快速修改并看到反馈。“在个人电脑上复制整个生产环境”这件事,只有当个人电脑的性能高到一定程度时才有可能的。在90年代之前,电信、金融、医疗、航天、军事等主要的IT用户,其生产环境是很难复制的。所以当时的软件开发过程必须是瀑布式的,因为修改之后看到效果的反馈周期太长,只好尽量提前把设计做好,开发一次成功。而90年代个人电脑性能的提升、尤其是奔腾CPU的广泛应用,使“在个人电脑上复制整个生产环境”成为了可能。

而轻量级J2EE则是在这个时间节点上的关键一触,把可能性变成了现实。如果将业务逻辑分布在多个进程中,就不可避免地需要运行多个Java虚拟机进程(甚至多个应用服务器进程),而这些——在当时的标准下——庞大的进程会耗尽当时大多数桌面开发电脑的性能,从而使反馈周期变长。

能够在单台个人电脑上复制整个生产环境,这个能力开启了整个敏捷软件开发的想象空间。测试驱动开发成为可能,持续集成成为可能,用户代表跟开发团队的紧密沟通才有必要,每天的站会、每周的迭代展示和计划会议在这个反馈周期的节奏下才有意义,注重对话的故事卡在这个节奏下才显得重要。单一代码库、单一进程、不分布对象、服务器农场式部署的架构风格,其目的是尽可能地保障在单台个人电脑上复制整个生产环境的能力,从而显著缩短反馈周期。

这就是重构技术走进主流视野的时代背景:摩尔定律使个人电脑堪堪具备足够的计算能力来复制整个生产环境,敏捷社区迫不及待地用轻量级J2EE架构使“复制整个生产环境”成为现实,在缩短反馈周期的同时也使单一代码库不得不承载整个系统的职责,从而使整个代码库变得庞大且职责不单一。为了在这样的代码库上继续快速的“修改-反馈”循环,对这样的代码库进行内部质量保障和改进的技术——测试驱动开发、持续集成、重构——才变得重要。

那么,十多年以后,我们所处的时代背景发生了哪些变化?这些变化是否会使重构技术变得不再重要?

=== 第四更:今天的世界 (2016-01-13) ===

今天的商业IT环境用一个词就可以概括:不确定。不确定带来颠簸和变动,不确定滋生怀疑和恐惧。而这种不确定,从丰田决定用

大规模定制

的产品来服务消费者的需求、而非仅仅提供某种功能开始,就已经注定了。当产品的目的是提供某种功能,功能的要求和达成的方式是相对确定的,不确定的比例相对较小;而当产品的目的是满足消费者的需求乃至提供良好的体验,人的不理性、不逻辑性就开始占据更大的比重,从而使不确定性的比例显著增加。Martin Fowler在《

企业应用架构模式

》里讲,所谓“业务逻辑”,实际上是“业务不逻辑”——因为这些业务规则根本不合逻辑,所以它们才需要特别地被提出、被讨论、被理解、被确认。这个洞见反映出当代商业IT环境的特征:现在的商业IT,越来越多地是在建模与迎合人的不理性、不逻辑性。在这个背景下,不确定性的剧增就是不可避免的。

当不确定性还不算太多的时候,对软件的要求是可维护性:需要加新的功能、需要修改旧的功能,能改得进去。这种“维护”(或者叫“演化”)实际上仍然是基于预测的。整个软件系统的大致方向已经被预测了,然后在此基础上谈演化。而当不确定实在太多的时候,对软件的要求就变成了可抛弃性。整本《

精益创业

》讲的就是这么一回事:你构建(build)的所有东西,都是为了度量(measure)某种数据从而学习(learn)——“学习”的定义是证实或证伪某个假设(hypothesis),而一旦假设被证伪,就要立即转向(pivot)。换句话说,你开发的所有软件,都应该做好很快被抛弃的准备。

如何获得可抛弃性?很简单:少写代码。一万行代码扔掉要伤心,一千行代码随时扔掉重新写。从轻量级J2EE的胜利开始,开源软件在商用软件领域成为了绝对主流,并展现出巨大的复用优势:基础框架越来越成熟,应用编程越来越倾向于用DSL描述业务领域,代码量越来越少。Spring已经呈现出这个趋势,Ruby on Rails及之后的框架更将这个趋势推向极致:开发者可以在最短时间内以最少代码量做出一个规规矩矩的软件。开发这样的业务软件并不需要多少设计,因为大部分设计已经蕴含在框架内。代码结构简单,代码量少,决定了这样的代码库烂也烂不到哪里去。而且本来就是以可抛弃性为目标设计的软件,它的生命周期预期也不会长到让代码质量能烂到哪里去。

但是总有些系统会成功,会向着更复杂的方向演化。然而这时系统的演化就未必要在同一个代码库中进行了。在这个阶段,软件的设计者应该能区分出较为稳定的领域模型与较为易变的用户操作。领域模型与用户操作两者变化的频率不同,实现的技术也不同,完全没有理由存在于同一个代码库中。同时,多渠道、尤其是移动渠道的兴起,是另一个推动领域模型与用户操作分离的动因:领域模型最好是运行在自己的进程中,向各种渠道提供服务,从而在不同渠道之间复用领域逻辑。

于是我们再一次有了“以多个代码库、多个进程承载一个系统”的诉求。而这一次,摩尔定律的发展、特别是虚拟化技术的发展,使得“多进程”与“在个人电脑上复制整个生产环境”不再矛盾。正如Sam Newman在《

Building Microservices

》中所说:

Domain-driven design. Continuous delivery. On-demand virtualization. Infrastructure
automation. Small autonomous teams. Systems at scale. Microservices have emerged from
this world.

在同一本书里,Newman也提到了可抛弃性(他称之为“可替换性”)的问题:

Teams using microservice approaches are comfortable with completely rewriting services
when required, and just killing a service when it is no longer needed. When a codebase is
just a few hundred lines long, it is difficult for people to become emotionally attached to
it, and the cost of replacing it is pretty small.

既然不必把整个系统塞进一个代码库、一个进程,那么自然可以让每个代码库、每个进程符合

单一职责原则

,做且仅做一件事。这样的一个代码库规模不会大(尽管未必像Newman所想的只有几百行代码),在上面工作的团队也会很小。这样的一个代码库,它的质量不会烂到哪里去;即使出现了质量腐化的迹象,在有必要的测试覆盖的前提下,即使不遵循严格的重构手法也足以优化其内部质量;即使——尽管相当不可能——质量腐化到了相当的程度,正如Newman所说,从头写过就是了。在这样的一个代码库中,《重构》书中所描述的严格的重构技术所能发挥的价值将相当有限。

=== 第五更:尾声,以及新的开篇 (2016-01-15) ===

简单总结一下前面讲的内容:

  1. 《重构》所介绍的重构技术,是在不改变软件可观察行为的前提下,采用已经发表的重构手法,对单进程、单代码库内的软件结构进行调整。
  2. 这种技术适用于规模较大、生命周期较长、承担了较多责任、有一个较大(且较不稳定)团队在其上工作的单一代码库。
  3. 十多年前,企业应用架构转向单一代码库、单一进程、不分布对象、服务器农场式部署的架构风格,其目的是尽可能地保障在单台个人电脑上复制整个生产环境的能力,从而显著缩短反馈周期。
  4. 现在,更高的不确定性使代码库生命周期显著缩短,领域驱动设计和虚拟化技术使每个代码库职责单一。因此,在今天的商业IT环境下,重构技术的价值大大降低。

或者,换回哗众取宠的调调,我说的是:十五年以后,重构已死

清晰地认识到“重构已死”这一现实很重要。因为这个认识会让我们开启一系列更为宏大、影响更为深远的讨论:

  • 虽然单一代码库的职责变得简单,然而整个IT系统的复杂度仍然在。这些复杂度以何种方式表现
  • 虽然单一代码库的内部质量不至于严重腐化,然而整个复杂的IT系统仍然有可能腐化。一个复杂系统会以何种方式腐化
  • Opdyke在他的论文中介绍了一组行为保持的修改手法,以这些手法对程序进行修改时,对程序外部行为的影响可以被控制在最小范围。在代码之外,这些行为保持的修改手法是否普遍适用
  • 测试驱动开发是重构的安全网。在无法使用xUnit的情景下,如何构建安全网
  • Kerievsky在《重构与模式》中指出,重构以设计模式为目标。在面向对象设计模式不适用的场景下,如何确定重构的终点

我的观点是,虽然《重构》书中所介绍的重构技术已经过时,然而对坏味道重构手法的总结和抽象将指导我们得出广泛适用的重构思想,这种思想将有助于识别和解决其它复杂IT系统中的内部质量问题。并且由于康威法则(

Conway's law

)的作用,这种思想也将有助于识别和解决其它复杂组织系统中的内部质量问题。

用哗众取宠的调调来说就是:重构已死,重构思想永生

也许我会就这个题目写一本书。但愿我能写出来吧。

以上。