核心要點
虛構場景:黑客與矩陣
以下對話來自《黑客帝國》三部曲的一個被刪減的場景:
墨菲斯:尼奧,我現在就在矩陣裡。很抱歉要告訴你這個壞消息,但我們的特工追踪 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 規定,在子類中重寫方法時,必須滿足以下要求:
現在,請隨意再次閱讀上面的列表(別擔心,我會等),您希望能夠明白為什麼這很有道理。回到示例中,尼奧的致命錯誤只是沒有保持方法簽名相同,從而破壞了與客戶端代碼的契約。為了解決這個問題,特工映射器的 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中文網其他相關文章!