一、遗留应用

在其最简单的定义中,遗留应用程序是作为开发人员从其他人继承的任何应用程序。它是在你来之前写的,你对它是如何建造的几乎没有决策权。

然而,在开发人员中,legacy 这个词的重要性要大得多。它包含着组织不良、难以维护和改进、难以理解、未经测试或不稳定以及一系列类似的负面影响。该应用程序作为一种产品工作,因为它提供收入,但作为一个程序,它是脆弱的,对变化敏感。

因为这是一本专门介绍基于 PHP 的遗留应用程序的书,所以我将提供一些我在该领域中看到的特定于 PHP 的特性。出于我们的目的,PHP 中的遗留应用程序与以下两种或多种描述相匹配:

  • 它使用直接放在 web 服务器的文档根目录中的页面脚本。
  • 它在某些目录中有特殊的索引文件,以防止访问这些目录。
  • 如果没有设置某个值,则在某些文件的顶部有特殊的逻辑设置为die()exit()
  • 它的体系结构是面向包含的,而不是面向类或面向对象的。
  • 它的课程相对较少。
  • 存在的任何类结构都是无组织的、脱节的,并且在其他方面是不一致的。
  • 它更依赖函数而不是类方法。
  • 它的页面脚本、类和函数将模型、视图和控制器的关注点合并到同一范围内。
  • 它显示了一次或多次未完成重写尝试的证据,有时作为失败的框架集成。
  • 它没有供开发人员运行的自动化测试套件。

这些特性对于任何必须处理非常旧的 PHP 应用程序的人来说可能都很熟悉。它们描述了我所谓的典型 PHP 应用程序。

典型的 PHP 应用程序

大多数 PHP 开发人员没有经过正式的程序员培训,或者几乎完全是自学成才。他们通常来自其他非技术性的职业。不知何故,他们被赋予了创建网页的责任,因为他们被视为组织中最有技术头脑的人。由于 PHP 是一种非常宽容的语言,在没有太多规则的情况下提供了很多功能,因此在不经过大量培训的情况下,很容易生成工作网页甚至应用程序。

这些和其他因素极大地影响了典型 PHP 应用的基础。它们通常不是在流行的全堆栈框架中编写的,甚至不是在微框架中编写的。相反,它们通常是一系列页面脚本,直接放在 web 服务器文档根目录中,客户端可以直接浏览到这些脚本。任何需要重用的功能都已收集到一系列include文件中。有include文件用于常见配置和设置、页眉和页脚、常见表单和内容、函数定义、导航等。

在典型的 PHP 应用程序中,这种对include文件的依赖使我将其称为面向包含的体系结构。遗留应用程序到处使用include调用,将程序的各个部分耦合成一个整体。这与面向类的体系结构相反,在面向类的体系结构中,即使应用程序不遵循良好的面向对象编程原则,至少行为也会绑定到类中。

文件结构

典型的面向包含的 PHP 应用程序通常如下所示:

/path/to/docroot/
bin/                         # command-line tools
cache/                    # cache files
common/                # commonly-used include files
classes/                 # custom classes
Image.php            #
Template.php       #
functions/             # custom functions
db.php                 #
log.php                #
cache.php           #
setup.php            # configuration and setup
css/                     # stylesheets
img/                    # images
index.php           # home page script
js/                       # JavaScript
lib/                     # third-party libraries
log/                    # log files
page1.php        # other page scripts
page2.php        #
page3.php        #
sql/                   # schema migrations
sub/                  # sub-page scripts
index.php         #
subpage1.php #
subpage2.php #
theme/             # site theme files
header.php      # a header template
footer.php        # a footer template
nav.php           # a navigation template ~~

所示结构是一个简化示例。有许多可能的变化。在一些遗留的应用程序中,我已经看到了数百个主要级别的页面脚本和数十个子目录,它们有自己独特的附加页面层次结构。关键是遗留应用程序通常位于文档根目录中,具有用户直接浏览的页面脚本,并使用include文件管理大多数程序行为,而不是类和对象。

页面脚本

遗留应用程序将使用单个页面脚本作为公共行为的访问点。每个页面脚本负责设置全局环境,执行请求的逻辑,然后将输出传递给客户端。

附录 A典型遗留页面脚本包含真实应用程序中典型遗留页面脚本的净化、匿名版本。我已经自由地使缩进保持一致(最初,缩进有点随机),并将其包装为 60 个字符,以便更好地适应电子阅读器屏幕。现在去看看,但要小心。如果你因此失明或经历创伤后压力,我不承担任何责任!当我们检查时,我们发现各种各样的问题使得维护和改进变得困难:

  • 执行设置和表示逻辑的include语句
  • 内联函数定义
  • 全局变量
  • 模型、视图和控制器逻辑都组合在一个脚本中
  • 信任用户输入
  • 可能的 SQL 注入漏洞
  • 可能的跨站点脚本漏洞
  • 不带引号的数组键生成通知
  • if块没有用大括号包装(稍后在块中添加一行实际上不会是块的一部分)
  • 复制粘贴重复

就遗留页面脚本而言,附录 A典型遗留页面脚本示例相对平淡。我见过其他混合了 JavaScript 和 CSS 代码的脚本,还有远程文件包含和各种安全缺陷。它也只有(!约 400 行长。我见过数千行的页面脚本,它们生成了几种不同的页面变体,所有这些脚本都被包装成一个包含十几个或更多个case条件的switch语句。

重写还是重构?

许多开发人员在面对一个典型的 PHP 应用程序时,只能适应它这么长时间,然后他们才想放弃它,从头开始重写它。把它从轨道上炸开;这是唯一确定的方法!这是这些充满热情和活力的程序员的口号。其他开发者,他们的热情被死亡游行的经历所耗尽,对这样的建议感到谨慎和谨慎。他们完全知道代码库是坏的,但是他们知道的魔鬼(或者在我们的例子中,代码)比他们不知道的魔鬼要好。

重写的利弊

完全重写是一个非常诱人的想法。支持重写的开发人员感觉他们将能够在第一时间完成所有正确的事情。他们将能够编写单元测试、实现最佳实践、根据现代模式定义分离关注点、使用最新的框架甚至编写自己的框架(因为他们最了解自己的需求)。因为现有的应用程序可以作为参考实现,所以他们相信在重写应用程序时很少或根本没有尝试和错误工作。需要的行为已经存在;开发人员需要做的就是将它们复制到新系统中。在现有系统中难以或不可能实现的行为可以作为重写的一部分从一开始就添加。

尽管重写听起来很诱人,但它充满了许多危险。Joel Spolsky 在 2000 年对旧的 Netscape Navigator web 浏览器进行重写时说:

|   | 网景公司通过决定从头重写代码,犯下了任何软件公司都可能犯的最严重的战略错误。Lou Montulli 是制作最初版本 Navigator 的 5 位编程巨星之一,他给我发电子邮件说,我完全同意,这是我从 Netscape 辞职的主要原因之一。这一决定花费了网景 3 年的时间。在这三年中,微软无法添加新功能,无法响应来自 Internet Explorer 的竞争帖子,不得不坐在他们的手上,而微软则完全吃了他们的午餐。 |   | |   | --Joel Spolsky,网景疯狂 |

结果,网景公司倒闭了。

Josh Kerr 讲述了一个关于 TextMate 的类似故事:

|   | Macromates,一家拥有一个非常成功的文本编辑器 Textmate 的独立公司,决定重写 Textmate 2 的代码库。他们花了 6 年的时间才发布了一个 beta 版,这在今天是一个永恒的时代,他们失去了很多市场份额。当他们发布测试版时,为时已晚,6 个月后他们放弃了该项目,并将其作为开源项目推到 Github 上。 |   | |   | --Josh Kerr,TextMate 2 以及为什么不重写代码 |

弗雷德布鲁克斯将彻底重写的冲动称为第二个系统效应。他在 1975 年写道:

|   | 第二个是人类设计的最危险的系统。。。总的趋势是过度设计第二个系统,使用第一个系统上谨慎偏离的所有想法和虚饰。。。第二个系统效应是。。。一种改进技术的趋势,其存在本身已被基本系统假设的变化所淘汰。。。项目经理如何避免第二种系统效应?通过坚持让一位至少拥有两个系统的高级架构师来完成。 |   | |   | --弗雷德·布鲁克斯,《神秘的人类月》,第 53-58 页。 |

四十年前的开发者和今天一样。我预计在未来四十年里,它们也会是一样的;人仍然是人。过度自信、悲观不足、对历史的无知,以及想成为自己的客户的愿望,都很容易让开发人员做出合理化的解释,这一次他们尝试重写时会有所不同。

为什么不重写作品?

重写很少奏效的原因有很多,但这里我只集中讨论一个一般原因:资源、知识、沟通和生产力的交叉点。(请务必阅读神话中的人月(第 13-26 页),了解与将资源和调度视为可互换元素相关的问题的详细描述。)

与所有事情一样,我们只有有限的资源来承担重写项目。组织中只有一定数量的开发人员。这些开发人员将不得不对现有程序进行维护,并编写程序的全新版本。在一个项目上工作的任何开发人员都不能在另一个项目上工作。

语境转换问题

一个想法是让现有开发人员将一部分时间花在旧应用程序上,另一部分时间花在新应用程序上。然而,在两个项目之间移动一个开发人员并不是生产力的平均分割。由于上下文转换的认知负载,开发人员在每个上下文转换上的效率都不到一半。

知识问题

为了避免在维护和重写之间切换开发人员造成的生产力损失,组织可能会尝试雇佣更多的开发人员。然后,可以将一些专用于旧项目,而另一些专用于新项目。不幸的是,这种方法揭示了哈耶克所谓的知识问题。知识问题最初应用于经济学领域,同样适用于编程。

如果我们让新开发人员参与重写项目,他们将对现有系统、现有问题、业务目标了解不够,甚至可能不知道有效地进行重写的最佳实践。他们必须接受这些方面的培训,最有可能的是由现有的开发人员进行培训。这意味着现有的开发人员,他们已经降级到维护现有的程序,将不得不花费大量的时间与新员工交流知识。所涉及的时间是不平凡的,在新开发人员与现有开发人员一样精通之前,这些知识的交流将不得不继续。这意味着资源的线性增长导致生产力的线性增长不到:程序员数量的 100%增长将导致产出增长不到 50%,有时甚至更少(参见人月痛苦的数学http://paul-m-jones.com/archives/1591 ).

或者,我们可以让现有的开发人员参与重写项目,让新员工负责维护现有的程序。这也暴露了一个知识问题,因为新开发人员对系统完全不熟悉。他们从哪里获得工作所需的知识?当然,现有的开发人员仍然需要花费宝贵的时间将他们的知识传达给新员工。我们再次看到,开发人员的线性增长导致生产力的线性增长。

进度问题

为了解决知识问题和相关的沟通成本,一些人可能认为处理该项目的最佳方式是让所有现有开发人员都参与重写,并推迟现有系统的维护和升级,直到重写完成。这是一个巨大的诱惑,因为开发人员将非常渴望减轻自己的痛苦,成为自己的客户——对他们想要的功能和他们想要的修复感到兴奋。这些欲望会导致他们高估自己执行完全重写的能力,而低估完成重写所需的时间。对于管理者来说,他们会接受开发人员的乐观态度,也许会在进度表中添加一些缓冲,以便更好地衡量。

当开发人员意识到任务实际上比他们最初想象的要艰巨得多时,他们的过度自信和乐观会演变成挫折和痛苦。重写将比预期的时间长得多,不是一点点,而是一个数量级或更多。在重写的过程中,现有的程序将受到影响——有缺陷和功能缺失——让现有客户失望,无法吸引新客户。重写项目最终将成为一场恐慌的死亡游行,不惜一切代价完成它,结果将是一个与第一个一样糟糕的代码库,只是方式不同。它将仅仅是第一个系统的一个副本,因为时间表的压力将决定新的特性将被推迟到初始版本实现之后。

迭代重构

考虑到与完全重写相关的风险,我建议改为重构。重构意味着在不改变程序的功能的情况下,程序的质量在小步中得到提高。在整个系统中引入一个相对较小的更改。然后对该系统进行测试,以确保其仍能正常工作,最后,该系统投入生产。第二个小变化建立在前一个变化的基础上,依此类推。经过一段时间后,该系统变得更易于维护和改进。

重构方法显然不如完全重写吸引人。它挑战了大多数开发人员的核心敏感性。开发人员必须在很长一段时间内继续使用这个系统,不管它有什么缺点。他们无法切换到最新、最热门的框架。他们不能成为自己的客户,也不能放纵自己的欲望,在第一时间就把事情做好。作为一种长期战略,重构方法不适合那种重视快速开发新应用程序而不是修补现有应用程序的文化。开发人员通常更喜欢启动自己的新项目,而不是维护他人开发的旧项目。

然而,作为一种降低风险的策略,使用迭代重构方法无疑优于重写。与重写项目的任何类似部分相比,单个重构本身都很小。它们的应用时间比重写时的可比特性要短得多,并且在每次迭代结束时,它们使现有的代码库处于工作状态。现有应用程序在任何时候都不会停止运行或进行。迭代重构可以集成到一个更大的流程中,并进行调度,以允许错误修复、功能添加和重构的周期,从而改进下一个周期。

最后,任何重构步骤的目标都不是完美。每一步的目标仅仅是改进。我们并不是试图在很长一段时间内实现一个不可能实现的目标。我们正在朝着可以在短时间内实现的容易可视化的目标迈出一小步。每一次小的重构胜利都将提高士气,并激发对下一个重构步骤的热情。随着时间的推移,这些小的胜利累积成了一个大的胜利:一个完全现代化的代码库,从未停止为业务创造收入。

遗留框架

到目前为止,我们一直在将遗留应用程序作为基于页面、面向包含的系统进行讨论。然而,也有大量使用公共框架的遗留代码。

基于框架的遗留应用程序

PHP 领域中每一个不同的公共框架都是它自己独特的地狱。使用CakePHP编写的应用程序 http://cakephp.org/ )与 CodeIgniter、Solar、Symfony 1、Zend Framework 1 等中编写的版本相比,它的存在不同的遗留问题。每一种不同的框架,以及它们不同的工作类型,都会在应用程序中鼓励不同类型的紧密耦合。因此,重构使用这些框架之一构建的应用程序所需的具体步骤与重构不同框架所需的步骤非常不同。

因此,本书的各个部分可以作为基于公共框架重构遗留应用程序不同部分的指南,但总体而言,本书并不针对基于这些公共框架重构应用程序。

内部、私有或其他非公共框架由组织内自己的架构师直接控制,可能从本书中包含的重构中获益。

重构到框架

我有时听到开发人员如何明智地希望避免完全重写,而是希望重构或迁移到公共框架。这听起来像是两全其美,将迭代方法与开发人员使用最热门新技术的愿望结合起来。

我对遗留 PHP 应用程序的经验是,它们对框架集成的抵抗力几乎和对单元测试的抵抗力一样。如果应用程序已经处于逻辑可以移植到框架的状态,那么首先就不需要移植它。

然而,当我们完成本书中的重构时,应用程序很可能处于更适合公共框架迁移的状态。开发商是否仍愿意这样做是另一回事。

回顾和下一步

在这一点上,我们已经意识到重写虽然有吸引力,但却是一种危险的方法。迭代重构方法听起来更像是实际工作,但其优点是可实现且现实。

下一步是通过排除一些先决条件,为重构方法做好准备。之后,我们将通过一系列相对较小的步骤来实现遗留应用程序的现代化,每章一步,每一步分解为一个易于遵循的过程,并回答常见问题。

让我们开始吧!