ホームページ  >  記事  >  バックエンド開発  >  PHPコードのリファクタリング方法のまとめ

PHPコードのリファクタリング方法のまとめ

php中世界最好的语言
php中世界最好的语言オリジナル
2018-05-16 15:54:111734ブラウズ

今回は、PHPコードのリファクタリング方法についてまとめてみました。PHPコードのリファクタリングにおける注意点とは何でしょうか?実際の事例を見てみましょう。

PHP が単純なスクリプト言語から成熟した プログラミング言語 に進化するにつれて、典型的な PHP アプリケーションのコード ベースは複雑になってきました。これらのアプリケーションのサポートとメンテナンスを制御するために、さまざまなテスト ツールを使用してプロセスを自動化できます。その 1 つは単体テストで、これを使用すると、作成したコードの正確性を直接テストできます。ただし、多くの場合、従来のコード ベースはこの種のテストには適していません。この記事では、コードベースの改善への依存関係を減らしながら、一般的な単体テスト ツールを使用したテストのプロセスを簡素化するために、FAQ を含む PHP コードのリファクタリング戦略を紹介します。

はじめに

PHP の開発の歴史を振り返ると、PHP は、当時人気のあった CGI スクリプトを置き換えるために使用されていた単純な動的スクリプト言語から、成熟した現代のプログラミング言語に変化したことがわかります。 コードベースが増大するにつれて、手動テストは不可能な作業となり、コードに対するすべての変更は、大小を問わず、アプリケーション全体に影響を及ぼします。これらの影響は、特定のページの読み込みやフォームの保存に影響を与えるだけの小さなものである場合もあれば、検出が難しい問題が発生したり、特定の条件下でのみ発生するエラーが発生したりする場合もあります。さらに、以前に修正された問題がアプリケーションで再発する可能性もあります。これらの問題を解決するために、多くのテスト ツールが開発されています。

一般的な方法の 1 つは、いわゆる機能テストまたは受け入れテストであり、典型的なユーザー操作を通じてアプリケーションをテストします。これはアプリケーション内の個々のプロセスをテストする優れた方法ですが、テスト プロセスが非常に遅くなる可能性があり、一般に、基になるクラスやメソッドが期待どおりに動作するかどうかのテストに失敗します。現時点では、単体テストという別のテスト方法を使用する必要があります。単体テストの目的は、アプリケーションの基礎となるコードの機能をテストし、実行後に正しい結果が得られることを確認することです。多くの場合、これらの「成長する」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 = &#39;.&#39;;
  if ( !isset($thousands_separator) ) $thousands_separator = &#39;,&#39;;
  return number_format($number, $decimal_precision, $decimal_separator,
 $thousands_separator);
}

これらのグローバル変数は 2 つの異なる問題を引き起こします。最初の問題は、テストでこれらすべてのグローバル変数を考慮し、関数が受け入れられる有効な値に設定されていることを確認する必要があることです。 2 番目のより深刻な問題は、後続のテストの状態を変更したり、その結果を無効にしたりできないことです。グローバル状態がテスト実行前の状態にリセットされていることを確認する必要があります。 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 = &#39;.&#39;;
  if ( !isset($thousands_separator) ) $thousands_separator = &#39;,&#39;;
  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(&#39;localhost&#39;,&#39;user&#39;,&#39;password&#39;);
    $this->results = $dbconn->query(&#39;select name from mytable&#39;);
  }
  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(&#39;localhost&#39;,&#39;user&#39;,&#39;password&#39;);
    $this->results = $dbconn->query(&#39;select name from mytable&#39;);
  }
  public function getFirstResult()
  {
    return $this->results[0];
  }
}

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

经硬编码的类依赖性

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

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

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

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

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

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

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

可测试代码的好处

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

相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!

推荐阅读:

php无限级评论嵌套实现步骤详解

php实现数据库增删查改步骤详解

以上がPHPコードのリファクタリング方法のまとめの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。