核心要点
虚构场景:黑客与矩阵
以下对话来自《黑客帝国》三部曲的一个被删减的场景:
墨菲斯:尼奥,我现在就在矩阵里。很抱歉要告诉你这个坏消息,但我们的特工追踪 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中文网其他相关文章!