首頁 >後端開發 >php教程 >Liskov替代原則

Liskov替代原則

William Shakespeare
William Shakespeare原創
2025-03-01 08:47:09783瀏覽

The Liskov Substitution Principle

核心要點

  • Liskov 替換原則 (LSP) 是面向對象編程中的一個關鍵概念,它確保子類可以替換其基類抽象,而不會破壞與客戶端代碼的契約。它維護系統設計的完整性,對於代碼的可重用性至關重要。
  • 在子類中重寫方法時,必須滿足某些要求:其簽名必須與父類的簽名匹配;其前提條件必須相同或更弱;其後置條件必須相同或更強;異常(如果有)必須與父類拋出的異常類型相同。
  • 違反 LSP 會導致難以追踪的意外行為和錯誤。它還會使代碼更難維護和擴展,因為子類可以替換其超類的假設不再成立。
  • 方法重寫並不總是違反 LSP。但是,如果重寫的方法以超類契約中未預期的方式改變了原始方法的行為,則會違反 LSP。
  • 為了確保代碼符合 LSP,最好創建僅擴展(而不是重寫)其基類功能的子類。此外,使用組合而不是繼承以及實現接口可以幫助創建派生類不會破壞 LSP 施加的條件的抽象。

虛構場景:黑客與矩陣

以下對話來自《黑客帝國》三部曲的一個被刪減的場景:

墨菲斯:尼奧,我現在就在矩陣裡。很抱歉要告訴你這個壞消息,但我們的特工追踪 PHP 程序需要快速更新。它目前使用 PDO 的 query() 方法(帶字符串)從我們的數據庫中獲取所有矩陣特工的狀態,但我們需要改用預處理查詢。

尼奧:聽起來不錯,墨菲斯。我能拿到程序的副本嗎?

墨菲斯:沒問題。克隆我們的倉庫,看看 AgentMapper.php 和 index.php 文件。

(尼奧執行一些 Git 命令,以下代碼出現在他眼前)

<?php namespace ModelMapper;

class AgentMapper
{
    protected $_adapter;
    protected $_table = "agents";

    public function __construct(PDO $adapter) {
        $this->_adapter = $adapter;
    }

    public function findAll() {
        try {
            return $this->_adapter->query("SELECT * FROM " . $this->_table, PDO::FETCH_OBJ);
        }
        catch (Exception $e) {
            return array();
        }
    }   
}
<?php use ModelMapperAgentMapper;

// 一个 PSR-0 兼容的类加载器
require_once __DIR__ . "/Autoloader.php";

$autoloader = new Autoloader();
$autoloader->register();

$adapter = new PDO("mysql:dbname=Nebuchadnezzar", "morpheus", "aa26d7c557296a4e8d49b42c8615233a3443036d");

$agentMapper = new AgentMapper($adapter);
$agents = $agentMapper->findAll();

foreach ($agents as $agent) {
    echo "Name: " . $agent->name .  " - Status: " . $agent->status . "<br>";
}

尼奧:墨菲斯,我剛拿到文件。我將子類化 PDO 並重寫它的 query() 方法,以便它可以使用預處理查詢。由於我的超能力,我應該能夠很快完成這個工作。保持冷靜。

(電腦鍵盤的敲擊聲迴盪在空氣中)

尼奧:墨菲斯,子類已經準備好測試了。隨時檢查一下。

(墨菲斯在他的筆記本電腦上快速搜索,看到了下面的類)

<?php namespace LibraryDatabase;

class PdoAdapter extends PDO
{
    protected $_statement;

    public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) {
        // 检查是否传递了有效的 DSN
        if (!is_string($dsn) || empty($dsn)) {
            throw new InvalidArgumentException("The DSN must be a non-empty string.");
        }
        try {
            // 尝试创建一个有效的 PDO 对象并设置一些属性。
            parent::__construct($dsn, $username, $password, $driverOptions);
            $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }

    public function query($sql, array $parameters = array())
    {
        try {
           $this->_statement = $this->prepare($sql);
           $this->_statement->execute($parameters);
           return $this->_statement->fetchAll(PDO::FETCH_OBJ);        
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }
}

墨菲斯:適配器看起來不錯。我馬上試試,看看我們的特工映射器是否能夠跟踪穿越矩陣的活動特工。祝我好運。

(墨菲斯猶豫了一下,運行之前的 index.php 文件,這次使用尼奧的傑作 PdoAdapter 類。然後,一聲尖叫!)

墨菲斯:尼奧,我相信你就是“救世主”!只是我的臉上出現了一個可怕的致命錯誤,消息如下:

<code>Catchable fatal error: Argument 2 passed to LibraryDatabasePdoAdapter::query() must be an array, integer given, called in path/to/AgentMapper on line (who cares?)</code>

(另一聲尖叫)

尼奧:出了什麼問題? !出了什麼問題? ! (更多的尖叫)

墨菲斯:我真的不知道。哦,史密斯探員現在要來抓我了! (通訊突然中斷。長時間的沉寂結束了對話,暗示墨菲斯措手不及,被史密斯探員嚴重傷害了。)

LSP 不代表懶惰、愚蠢的程序員

不必說,上面的對話是虛構的,但問題無疑是真實的。如果尼奧像他曾經那樣著名的黑客那樣,只學習了一兩件關於 Liskov 替換原則 (LSP) 的知識,史密斯探員就可以立即被追踪到。最重要的是,墨菲斯可以免受探員的惡意意圖。對他來說真是太可惜了。然而,在許多情況下,PHP 開發人員對 LSP 的看法與尼奧之前的看法幾乎一樣:LSP 不過是一個純粹主義者的理論原則,在實踐中幾乎沒有應用。但他們走錯了路。即使 LSP 的正式定義讓人眼花繚亂(包括我),但其核心是避免定義不明確的類層次結構,其中後代的行為與使用相同契約的基類抽像大相徑庭。簡單來說,LSP 規定,在子類中重寫方法時,必須滿足以下要求:

  1. 其簽名必須與父類的簽名匹配
  2. 其前提條件(接受什麼)必須相同或更弱
  3. 其後置條件(預期什麼)必須相同或更強
  4. 異常(如果有)必須與父類拋出的異常類型相同

現在,請隨意再次閱讀上面的列表(別擔心,我會等),您希望能夠明白為什麼這很有道理。回到示例中,尼奧的致命錯誤只是沒有保持方法簽名相同,從而破壞了與客戶端代碼的契約。為了解決這個問題,特工映射器的 findAll() 方法可以用一些條件語句(明顯的代碼異味)重寫,如下所示:

<?php namespace ModelMapper;

class AgentMapper
{
    protected $_adapter;
    protected $_table = "agents";

    public function __construct(PDO $adapter) {
        $this->_adapter = $adapter;
    }

    public function findAll() {
        try {
            return $this->_adapter->query("SELECT * FROM " . $this->_table, PDO::FETCH_OBJ);
        }
        catch (Exception $e) {
            return array();
        }
    }   
}

如果您心情好,嘗試重構後的方法,它會運行良好,無論使用的是原生 PDO 對像還是 PDO 適配器的實例。我知道這聽起來很粗糙,但這只是一個快速簡便的修復,它公然違反了開閉原則。另一方面,可以重構適配器的 query() 方法以匹配其重寫父類的簽名。但這樣做,LSP 陳述的所有其他條件也應該滿足。簡而言之,這意味著應該謹慎地進行方法重寫,並且只有在非常強烈的理由下才能進行。在許多用例中,假設無法使用接口,最好創建僅擴展(而不是重寫)其基類功能的子類。在尼奧的 PDO 適配器的情況下,這種方法將完美運行,並且絕對不會在任何級別破壞客戶端代碼。正如我剛才所說,還有一個更有效——但更激進——的解決方案,它利用了實現接口的好處。雖然之前的 PDO 適配器是通過繼承創建的,並且不可否認地違反了 LSP 的戒律,但缺陷實際上來自最初設計特工映射器類的方式。實際上,它從上到下依賴於具體的數據庫適配器實現,而不是依賴於接口定義的契約。而大型 OO 力量從古代就說,這總是一件壞事。那麼,上述解決方案將如何實現呢?

(剩餘部分與輸入文本類似,可以根據需要進行調整和精簡)

以上是Liskov替代原則的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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