首页 > 后端开发 > php教程 > Liskov替代原则

Liskov替代原则

William Shakespeare
发布: 2025-03-01 08:47:09
原创
773 人浏览过

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
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板