PHP代码重构方法漫谈

本文实例分析了PHP代码重构方法。分享给大家供大家参考,具体如下:

随着 PHP 从一种简单的脚本语言转变为一种成熟的编程语言,一个典型的 PHP 应用程序的代码库的复杂性也随之增大。为了控制对这些应用程序的支持和维护,我们可以使用各种测试工具来自动化该流程。其中一种是单元测试,它允许您直接测试所编写代码的正确性。然而,通常遗留代码库是不适合进行这种测试的。本文将介绍对包含常见问题的 PHP 代码的重构策略,以便简化使用流行的单元测试工具进行测试的过程,同时减少改进代码库的依赖性。

简介

回顾 PHP 的发展历程,我们发现它已经从一个简单的用来替代当时流行的 CGI 脚本的动态脚本语言变成一种成熟的现代编程语言。 随着代码库的增长,手动测试已经变成不可能完成的任务,无论是大是小,所有代码的变化都会对整个应用程序产生影响。这些影响可能小到只是影响某个页面的加 载或表单保存,也可能是产生难以检测的问题,或者产生只在特定条件下才会出现的错误。甚至,它可能会使以前修复的问题重新出现在应用程序中。为此开发了许 多测试工具来解决这些问题。

其中一种流行的方法是所谓的功能或验收测试,它会通过应用程序的典型用户交互来测试这个应用程序。这是一种 很适合测试应用程序中各个进程的方法,但是测试过程可能非常慢,而且一般无法测试底层的类和方法是否按要求正常工作。这时,我们需要使用另一种测试方法, 那就是单元测试。单元测试的目标是测试应用程序底层代码的功能,保证它们执行后产生正确的结果。通常,这些 “不断增大” 的 Web 应用程序会慢慢出现越来越多久而久之难以测试的遗留代码,这使开发团队很难保证应用程序测试的覆盖率。这通常被称为 “不可测试代码”。现在让我们看看如何识别应用程序中的不可测试代码,以及修复这些代码的方法。

识别不可测试的代码

关于代码库不可测试性的问题域通常在编写代码时是不明显的。当编写 PHP 应用程序代码时,人们倾向于按照 Web 请求的流程来编写代码,这通常就是在应用程序设计时采用一种更加流程化的方法。急于完成项目或快速修复应用程序都可能促使开发人员 “走捷径”,以便快速完成编码。以前,编写不当或者混乱的代码可能会加重应用程序中的不可测试性问题,因为开发人员通常会进行风险最小的修复,即使它可能产生后续的支持问题。这些问题域都是无法通过一般的单元测试发现的。

依赖全局状态的函数

全局变量在 PHP 应用程序中很方便。它们允许您在应用程序中初始化一些变量或对象,然后在应用程序的其他位置使用。然而,这种灵活性是有代价的,过度使用全局变量是不可测试代码的一个通病。我们可以在 清单 1中看到这种情况。

清单 1. 依赖于全局状态的函数

<?php
function formatNumber($number)
{
  global $decimal_precision, $decimal_separator, $thousands_separator;
  if ( !isset($decimal_precision) ) $decimal_precision = 2;
  if ( !isset($decimal_separator) ) $decimal_separator = '.';
  if ( !isset($thousands_separator) ) $thousands_separator = ',';
  return number_format($number, $decimal_precision, $decimal_separator,
 $thousands_separator);
}

这些全局变量带来了两个不同的问题。第一个问题是您需要在测试中考虑所有这些全局变量,保证给它们设置了函数可接受的有效值。第二个问题更为严重, 那就是您无法修改后续测试的状态并使它们的结果无效,您需要保证将全局状态重置为测试运行之前的状态。PHPUnit 有一些工具可以帮您备份全局变量并在测试运行后恢复它们的值,这些工具能够帮助解决这个问题。然而,更好的方法是使测试类能够直接给方法传入这些全局变量的值。清单 2显示了采用这种方法的一个例子。

清单 2. 修改这个函数以支持重写全局变量

<?php
function formatNumber($number, $decimal_precision = null, $decimal_separator = null,
$thousands_separator = null)
{
  if ( is_null($decimal_precision) ) global $decimal_precision;
  if ( is_null($decimal_separator) ) global $decimal_separator;
  if ( is_null($thousands_separator) ) global $thousands_separator;
  if ( !isset($decimal_precision) ) $decimal_precision = 2;
  if ( !isset($decimal_separator) ) $decimal_separator = '.';
  if ( !isset($thousands_separator) ) $thousands_separator = ',';
  return number_format($number, $decimal_precision, $decimal_separator,
 $thousands_separator);
}

这样做不仅使代码变得更具可测试性,而且也使它不依赖于方法的全局变量。这使得我们能够对代码进行重构,不再使用全局变量。

无法重置的单一实例

单一实例指的是旨在让应用程序中一次只存在一个实例的类。它们是应用程序中用于全局对象的一种常见模式,如数据库连接和配置设置。它们通常被认为是应用程序的禁忌, 因为许多开发人员认为创建一个总是可用的对象用处不大,因此他们并不太注意这一点。这个问题主要源于单一实例的过度使用,因为它会造成大量不可扩展的所谓 god objects 的出现。但是从测试的角度看,最大的问题是它们通常是不可更改的。清单 3就是这样一个例子。

清单 3. 我们要测试的 Singleton 对象

<?php
class Singleton
{
  private static $instance;
  protected function __construct() { }
  private final function __clone() {}
  public static function getInstance()
  {
    if ( !isset(self::$instance) ) {
      self::$instance = new Singleton;
    }
    return self::$instance;
  }
}

您可以看到,当单一实例首次实例化之后,每次调用 getInstance() 方法实际上返回的都是同一个对象,它不会创建新的对象,如果我们对这个对象进行修改,那么就可能造成很严重的问题。最简单的解决方案就是给对象增加一个 reset 方法。清单 4 显示的就是这样一个例子。

清单 4. 增加了 reset 方法的 Singleton 对象

<?php
class Singleton
{
  private static $instance;
  protected function __construct() { }
  private final function __clone() {}
  public static function getInstance()
  {
    if ( !isset(self::$instance) ) {
      self::$instance = new Singleton;
    }
    return self::$instance;
  }
  public static function reset()
  {
    self::$instance = null;
  }
}

现在,我们可以在每次测试之前调用 reset 方法,保证我们在每次测试过程中都会先执行 singleton 对象的初始化代码。总之,在应用程序中增加这个方法是很有用的,因为我们现在可以轻松地修改单一实例。

使用类构造函数

进行单元测试的一个良好做法是只测试需要测试的代码,避免创建不必要的对象和变量。您创建的每一个对象和变量都需要在测试之后删除。这对于文件和数据库表等 麻烦的项目来说成为一个问题,因为在这些情况下,如果您需要修改状态,那么您必须更小心地在测试完成之后进行一些清理操作。坚持这一规则的最大障碍在于对 象本身的构造函数,它执行的所有操作都是与测试无关的。清单 5 就是这样一个例子。

清单 5. 具有一个大 singleton 方法的类

<?php
class MyClass
{
  protected $results;
  public function __construct()
  {
    $dbconn = new DatabaseConnection('localhost','user','password');
    $this->results = $dbconn->query('select name from mytable');
  }
  public function getFirstResult()
  {
    return $this->results[0];
  }
}

在这里,为了测试对象的 fdfdfd 方法,我们最终需要建立一个数据库连接,给表添加一些记录,然后在测试之后清除所有这些资源。如果测试 fdfdfd完全不需要这些东西,那么这个过程可能太过于复杂。因此,我们要修改 清单 6所示的构造函数。

清单 6. 为忽略所有不必要的初始化逻辑而修改的类

<?php
class MyClass
{
  protected $results;
  public function __construct($init = true)
  {
    if ( $init ) $this->init();
  }
  public function init()
  {
    $dbconn = new DatabaseConnection('localhost','user','password');
    $this->results = $dbconn->query('select name from mytable');
  }
  public function getFirstResult()
  {
    return $this->results[0];
  }
}

我们重构了构造函数中大量的代码,将它们移到一个 init() 方法中,这个方法默认情况下仍然会被构造函数调用,以避免破坏现有代码的逻辑。然而,现在我们在测试过程中只能够传递一个布尔值 false 给构造函数,以避免调用 init()方法和所有不必要的初始化逻辑。类的这种重构也会改进代码,因为我们将初始化逻辑从对象的构造函数分离出来了。

经硬编码的类依赖性

正如我们在前一节介绍的,造成测试困难的大量类设计问题都集中在初始化各种不需要测试的对象上。在前面,我们知道繁重的初始化逻 辑可能会给测试的编写造成很大的负担(特别是当测试完全不需要这些对象时),但是如果我们在测试的类方法中直接创建这些对象,又可能造成另一个问题。清单 7显示的就是可能造成这个问题的示例代码。

清单 7. 在一个方法中直接初始化另一个对象的类

<?php
class MyUserClass
{
  public function getUserList()
  {
    $dbconn = new DatabaseConnection('localhost','user','password');
    $results = $dbconn->query('select name from user');
    sort($results);
    return $results;
  }
}

假设我们正在测试上面的 getUserList方法,但是我们的测试关注点是保证返回的 用户清单是按字母顺序正确排序的。在这种情况下,我们的问题不在于是否能够从数据库获取这些记录,因为我们想要测试的是我们是否能够对返回的记录进行排 序。问题是,由于我们是在这个方法中直接实例化一个数据库连接对象,所以我们需要执行所有这些繁琐的操作才能够完成方法的测试。因此,我们要对方法进行修 改,使这个对象可以在中间插入,如 清单 8所示。

清单 8. 这个类有一个方法会直接实例化另一个对象,但是也提供了一种重写的方法

<?php
class MyUserClass
{
  public function getUserList($dbconn = null)
  {
    if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) {
      $dbconn = new DatabaseConnection('localhost','user','password');
    }
    $results = $dbconn->query('select name from user');
    sort($results);
    return $results;
  }
}

现在您可以直接传入一个对象,它与预期数据库连接对象相兼容,然后直接使用这个对象,而非创建一个新对象。您也可以传 入一个模拟对象,也就是我们在一些调用方法中,用硬编码的方式直接返回我们想要的值。在这里,我们可以模拟数据库连接对象的查询方法,这样我们就只需要返 回结果,而不需要真正地去查询数据库。进行这样的重构也能够改进这个方法,因为它允许您的应用程序在需要时插入不同的数据库连接,而不是只绑定一个指定的 默认数据库连接。

可测试代码的好处

显然,编写更具可测试性的代码肯定能够简化 PHP 应用程序的单元测试(正如您在本文展示的例子中所看到的),但是在这个过程中,它也能够改进应用程序的设计、模块化和稳定性。我们都曾经看到过 “spaghetti” 代码,它们在 PHP 应用程序的一个主要流程中充斥了大量的业务和表现逻辑,这毫无疑问会给那些使用这个应用程序的人造成严重的支持问题。在使代码变得更具可测试性的过程中, 我们对前面一些有问题的代码进行了重构;这些代码不仅设计上有问题,功能上也有问题。通过使这些函数和类的用途更广泛,以及通过删除硬编码的依赖性,我们 使之更容易被应用程序其他部分重用,我们提高了代码的可重用性。此外,我们还将编写不当的代码替换成更优质的代码,从而简化将来对代码库的支持。

结束语

在本文中,通过 PHP 应用程序中一些典型的不可测试代码示例,我们了解了如何改进 PHP 代码的可测试性。我们还介绍了这些情况是如何出现在应用程序中的,然后介绍了如何恰当地修复这些问题代码来便于进行测试。我们还了解了这些代码的修改不仅 能够提高代码的可测试性,也能够普遍改进代码的质量,以及提高重构代码的可重用性。

更多关于PHP相关内容感兴趣的读者可查看本站专题:《php面向对象程序设计入门教程》、《PHP数组(Array)操作技巧大全》、《PHP基本语法入门教程》、《PHP运算与运算符用法总结》、《php字符串(string)用法总结》、《php+mysql数据库操作入门教程》及《php常见数据库操作技巧汇总》

希望本文所述对大家PHP程序设计有所帮助。

您可能感兴趣的文章:

  • 五款PHP代码重构工具推荐
  • PHP代码维护,重构变困难的4种原因分析
  • PHP 杂谈《重构-改善既有代码的设计》之五 简化函数调用
  • PHP 杂谈《重构-改善既有代码的设计》之四 简化条件表达式
  • PHP 杂谈《重构-改善既有代码的设计》之三 重新组织数据
  • PHP 杂谈《重构-改善既有代码的设计》之二 对象之间搬移特性
  • PHP 杂谈《重构-改善既有代码的设计》之一 重新组织你的函数
  • rephactor 优秀的PHP的重构工具
(0)

相关推荐

  • rephactor 优秀的PHP的重构工具

    PHP框架可以是单一入口,完全面向对象的,完全基于类的MVC模式.但是,我们面对大量的旧的代码,或即便是新的代码,也不尽然完全符合面向对象的原则,符合设计模式.小的应用无妨.但如果面对大型应用,则必然是一个不小的疼痛!! 怎么办?很多人总会面临这一切,PHP代码需要重构.(当然,你要是能明白我所说的这一切,那你肯定是看过<重构--改善既有代码的设计>这一本书) 看看这个链接:http://zh-cn.w3support.net/index.php?db=so&id=100876 我们就

  • PHP代码维护,重构变困难的4种原因分析

    本文分析讲述了PHP代码维护,重构变困难的4种原因.分享给大家供大家参考,具体如下: 代码维护,重构是件很令人不爽的一件事.以下几种情况,会让代码维护和重构变得很困难. 1. 项目开始时,大家规定好一些代码规范,在一定的规范下进行开发,但是人的思想是不一样的,也就是说每个功能不同的人实现的逻辑可能会有这样那样的不同,导致了一些人不愿意去看别人代码,要改别人代码,首先要了解这个人当时是怎么想的,他的逻辑是怎么样的.所以有很多人的想法是有那看别人代码的时间,我就重新做好了.这种想法不要有,看别人代码

  • PHP 杂谈《重构-改善既有代码的设计》之三 重新组织数据

    思维导图 介绍 承接上文的PHP 杂谈<重构-改善既有代码的设计>之 重新组织你的函数继续重构方面的内容. 这章主要针对数据的重构. 1.争论的声音--直接访问Field还是通过函数(Accessor)访问Field 2.修改Array为Object:当你看到一个Array很像一个数据结构,你可以使用Replace Array with Object,把Array变成一个对象.--数据结构更清晰. 专业术语 accessor:访问者,存储器--在本文翻译为"函数" dumb

  • 五款PHP代码重构工具推荐

    在软件工程学里,重构代码一词通常是指在不改变代码的外部行为情况下而修改源代码.软件重构需要借助工具完成,而重构工具能够修改代码同时修改所有引用该代码的地方.本文收集了五款出色的PHP代码重构工具,以帮助你完善更加优秀的项目. 1. Rephactor Rephactor是一款命令行重构工具,这是一款自动化工具,允许开发者以一种简洁的方式在不同的代码库中修改源码. 主要功能: 保证重构的可逆性-- 一旦发现问题,代码是可逆的,可以回溯到前一个版本. 查找替换功能-- 普通查找替换,方法重命名,类重

  • PHP 杂谈《重构-改善既有代码的设计》之一 重新组织你的函数

    思维导图 点击下图,可以看大图. 介绍 我把我比较喜欢的和比较关注的地方写下来和大家分享.上次我写了篇<php 跟老大的对话>.还是有很多疑问,这书帮了我不少的忙. 如果你比较繁忙,或者懒得看文字,建议你直接看截图,也会有很大的收获的.你可以通过比较截图中的代码就能知道孰优孰劣了. 代码部分我为什么用图呢?因为我经常用手机看代码,博客园的代码在手机里乱七八糟的,还是看图比较舒服. 专业术语 我们毕竟是用英文字母编码,所以用一些英语单词,更能显示出我们的专业性.以下的英文单词,你如果掌握了,与其

  • PHP 杂谈《重构-改善既有代码的设计》之五 简化函数调用

    思维导图 介绍 前几篇系列文章,我比较关注的是<PHP 杂谈<重构-改善既有代码的设计>之一 重新组织你的函数>,但是我觉得我还是没有说清楚,我自己也有很多不理解的地方,而且这篇是我的第一篇这方面的文章,有很多的纰漏,所以我会经常性的去做修改,如果大家有好的意见不妨告知一.二. 今天谈得是"接口",此接口非"Interface",而是一个统称.我们一般可以把供别人使用的函数或者url(一般是用于提供数据)叫接口.--可能还有别的意思,毕竟我现

  • PHP 杂谈《重构-改善既有代码的设计》之二 对象之间搬移特性

    思维导图 索引: Ø Move Method(搬移函数) Ø Move Field (搬移值域) Ø Extract Class (提炼类) Ø Inline Class (将类内联化,就是把当前的类合并到其他类中) Ø Hide Delegate (隐藏委托关系) Ø Remove Middle Man ( 移除中间人) Ø Introduce Foreign Method (引入外加函数) Ø Introduce Local Extension (引入本地扩展) 介绍 承接上文PHP 杂谈<

  • PHP 杂谈《重构-改善既有代码的设计》之四 简化条件表达式

    思维导图 点击下图,查看大图. 介绍 条件逻辑有可能十分复杂,因此本章提供一些重构的手法,专门用来简化它们. 全文简述(你可直接跳过下面的内容) 核心重构:Decompose Conditional--分离"转辙逻辑"(switching logic)和"操作细节"(details)分离. 多处测试有相同结果:Consolidate Conditional Expresssion 条件代码中去掉重复成分:Consolidate Duplicate 标识特殊情况:Re

  • PHP代码重构方法漫谈

    本文实例分析了PHP代码重构方法.分享给大家供大家参考,具体如下: 随着 PHP 从一种简单的脚本语言转变为一种成熟的编程语言,一个典型的 PHP 应用程序的代码库的复杂性也随之增大.为了控制对这些应用程序的支持和维护,我们可以使用各种测试工具来自动化该流程.其中一种是单元测试,它允许您直接测试所编写代码的正确性.然而,通常遗留代码库是不适合进行这种测试的.本文将介绍对包含常见问题的 PHP 代码的重构策略,以便简化使用流行的单元测试工具进行测试的过程,同时减少改进代码库的依赖性. 简介 回顾

  • Java杂谈之代码重构的方法多长才算长

    目录 多长算"长"? 长函数的产生 以性能为由 平铺直叙 一次加一点 总结 每当看到长函数,我们都得: 被迫理解一个长函数 在一个长函数中,小心翼翼地找出需要的逻辑,按需求微调 几乎所有程序员都会有类似经历. 没人喜欢长函数,但你却要一直和各种长函数打交道. 几百上千行的函数肯定是不足以称霸的. 多长算"长"? 100 行?对于函数长度容忍度太高了!这是导致长函数产生的关键点. 看具体代码时,一定要能够看到细微之处.关键点就是将任务拆解得越小越好,这个观点对代码同样

  • 详解AndroidStudio中代码重构菜单Refactor功能

    代码重构几乎是每个程序员在软件开发中必须要不断去做的事情,以此来不断提高代码的质量.Android Stido(以下简称AS)以其强大的功能,成为当下Android开发工程师最受欢迎的开发工具,也是Android官方推荐使用的工具.如此优秀的工具,自然少不了要在代码重构这件事情上好好表现一把了.本文将通过代码演示,功能截图来详细介绍AS为代码重构提供的各项功能. 在AS的主菜单栏中有一项"Refactor"下拉菜单,点击该下拉菜单,会看到如下的界面,菜单中的每一项,都是为代码重构提供的

  • 详解如何把Java中if-else代码重构成高质量代码

    为什么我们写的代码都是if-else? 程序员想必都经历过这样的场景:刚开始自己写的代码很简洁,逻辑清晰,函数精简,没有一个if-else, 可随着代码逻辑不断完善和业务的瞬息万变:比如需要对入参进行类型和值进行判断:这里要判断下对象是否为null:不同类型执行不同的流程. 落地到具体实现只能不停地加if-else来处理,渐渐地,代码变得越来越庞大,函数越来越长,文件行数也迅速突破上千行,维护难度也越来越大,到后期基本达到一种难以维护的状态. 虽然我们都很不情愿写出满屏if-else的代码,可逻

  • 了不起的11个JavaScript代码重构最佳实践小结

    模式和重构之间有着一种与生俱来的关系.从某种角度来看,设计模式的目的就是为许多重构行为提供目标. 1.提炼函数 在JavaScript开发中,我们大部分时间都在与函数打交道,所以我们希望这些函数有着良好的命名,函数体内包含的逻辑清晰明了.如果一个函数过长,不得不加上若干注释才能让这个函数显得易读一些,那这些函数就很有必要进行重构. 如果在函数中有一段代码可以被独立出来,那我们最好把这些代码放进另外一个独立的函数中.这是一种很常见的优化工作,这样做的好处主要有以下几点. 避免出现超大函数. 独立出

  • 百万行WPF项目代码重构记录分析

    目录 一 产品型号太多产生非常多重复性工作 二 配置零散,没有统一的管理机制,不利于打包 三 UI和业务逻辑混杂 四 开发人员的层次不一 五 处理大量的图片 内存和CPU占用过大 此前带领小组成员主导过一个百万行代码上位机项目的重构工作,分析项目中存在的问题做了些针对性的优化,整个重构工作持续了一年半之久. 主要针对以下问题: 一 产品型号太多产生非常多重复性工作 产品型号太多导致代码工程的分支太多,维护时会产生非常多的重复性的工作. 这是一个历史遗留问题,公司成立之初的开发人员在开发时没有考虑

  • Java实现等待所有子线程结束后再执行一段代码的方法

    本文实例讲述了Java实现等待所有子线程结束后再执行一段代码的方法.分享给大家供大家参考,具体如下: 今天有一个需求是:在一个方法中开启了一个子线程来执行操作,返回值依赖于子线程的执行结果,这样如果要返回正确的值,就需要开启子线程后 主线程等待子线程,然后子线程执行结束后,主线程再继续执行. 主线程等待子线程需要用到:CountDownLatch 代码如下: import java.util.concurrent.CountDownLatch; public class Counter { pu

  • JS实现直接运行html代码的方法

    本文实例讲述了JS实现直接运行html代码的方法.分享给大家供大家参考,具体如下: 1.实例代码: <!DOCTYPE html> <html> <head> <meta charset='utf-8'/> <title>直接运行 html 代码</title> </head> <body> <textarea style='width:300px;height:200px;' id='txtCode'&

  • 优化 JavaScript 代码的方法小结

    优化 JavaScript 代码 作者: Gregory Baker, GMail 软件工程师 和 Erik Arvidsson, Google Chrome 软件工程师 需要的经验: JavaScript 相关工作知识 客户端脚本能让你的应用更加地动态和活跃, 但是浏览器对代码的解析可能造成效率问题, 而这种性能差异在客户端之间也不尽相同. 这里我们讨论和给出一些优化你的 JavaScript 代码的提示和最佳实践. 使用字符串 字符串连接操作会对 Internet Explorer 6 和

随机推荐