首頁  >  文章  >  後端開發  >  PHP程式碼重構方法漫談

PHP程式碼重構方法漫談

不言
不言原創
2018-04-17 13:41:421333瀏覽

這篇文章主要介紹了PHP程式碼重構方法,結合實例形式較為詳細的分析了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 = &#39;.&#39;;
  if ( !isset($thousands_separator) ) $thousands_separator = &#39;,&#39;;
  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 = &#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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn