liskov替换原则_构造函数与打破Liskov替代原则

liskov替换原则

At the risk of being targeted by the PHP hate-mongers, I must confess that I’m pretty comfortable with PHP’s object model. I’m not that naïve to claim it’s a full-blown model exposing all the bells and whistles of other “fatter” players, such as C++ and Java, but despite of all its quirks, PHP delivers what it promises at face value. I would say however that the model is notoriously permissive considering the relaxed constraints it imposes when it comes to defining userland interfaces.

冒着被PHP仇恨贩子作为目标的风险,我必须承认我对PHP的对象模型相当满意。 我并不是很天真地宣称它是一个成熟的模型,它暴露了其他“轻率”玩家(如C ++和Java)的所有风吹草动,但是尽管有许多怪癖,PHP仍能兑现其承诺。 但是,我要说的是,考虑到在定义用户界面时所施加的宽松约束,该模型非常宽松。

One issue with the model’s “forgiveness” is that one fine day you may wake up in a whimsical mood and specify a constructor inside an interface just because you can, without a twinge of guilt. If you think this through, the existence of a constructor as part of an interface is pretty pointless. First off, every time you define a contract, you’re actually abstracting away the behavior of the concrete implementations and leaving out of the picture how they must be assembled down the road. Second, how in the world you can get client code consuming a constructor once the implementers have been instantiated? There’s no parallel universe in which you can do such things unless without sloppily polluting consumers with a bunch of new operators, which should live and breathe somewhere else according to the “isolate application logic from object construction” mantra.

模型的“宽恕”存在一个问题,就是美好的一天,您可能会以异想天开的心情醒来,并在接口内指定构造函数,这是因为您可以避免一团罪恶感。 如果您仔细考虑一下,那么构造函数作为接口一部分的存在是毫无意义的。 首先,每次定义合同时,您实际上是在抽象具体实现的行为,而忽略了如何在将来组装它们。 其次,一旦实例化实现者,世界上如何才能使客户代码消耗构造器? 没有并行的世界,您可以执行此类操作,除非不使用大量新操作符粗暴地污染消费者,这些操作符应根据“从对象构造中分离应用程序逻辑”的口号生活和呼吸。

Rants aside, the morale of the story can be boiled down to the following: “Object construction is not part of the contract honored by its implementers”.

除了花哨的事,故事的士气可以归结为以下几点:“对象构造不是其实现者所遵守的合同的一部分”。

It’s easier to grasp concepts by example rather than reading dull theory, so in this article I’ll be demonstrating from a practical standpoint how the implementation of different constructors down the same hierarchy isn’t a violation of the Liskov Substitution Principle, which is a reason why you shouldn’t fall into the temptation of tainting your interfaces with constructors.

通过示例来掌握概念比阅读枯燥的理论要容易得多,因此在本文中,我将从实践的角度演示如何在同一层次结构中实现不同的构造方法不违反Liskov替代原理 ,为什么您不应该陷入使用构造函数污染接口的诱惑。

LSP破损的神话 (The Myth of LSP Breakage)

I guess I’ve already made a valid point with regard to avoiding constructors in interfaces, a process that certainly helps to generate contracts that describe and prescribe the behavior of certain object types (a.k.a. Subtype Polymorphism) but not how to create the objects in question. It might be incidentally harder, however, to understand why overriding a parent constructor in a subclass, or eventually implementing a brand new one doesn’t violates the LSP.

我想我在避免接口中的构造函数方面已经提出了一个正确的观点,该过程肯定有助于生成描述和规定某些对象类型(又称为Subtype Polymorphism )行为的契约,但不能帮助创建有问题的对象。 但是,要理解为什么在子类中重写父构造函数或最终实现全新的构造函数不会违反LSP可能会更困难。

Let’s say we need to create a PDO adapter just by hanging a lightweight subclass from the native PDO. The adapter’s contract, along with the corresponding implementation, could look as follows:

假设我们只需要通过挂接本地PDO的轻量级子类来创建PDO适配器。 适配器的合同以及相应的实现如下所示:

<?php
namespace LibraryDatabase;

interface DatabaseAdapterInterface
{     
    public function executeQuery($sql, array $parameters = array());
    public function select($table, array $bind, $operator = "AND");
    public function insert($table, array $bind);
    public function update($table, array $bind, $where = "");
    public function delete($table, $where = "");
}
<?php
namespace LibraryDatabase;

class PdoAdapter extends PDO implements DatabaseAdapterInterface
{
    private $statement;   
    
    public function __construct($dsn, $username = null, $password = null, array $options = array()) {
        if (!extension_loaded("pdo")) {
            throw new InvalidArgumentException(
                "The adapter needs the PDO extension to be loaded.");
        }
        if (!is_string($dsn) || empty($dsn)) {
            throw new InvalidArgumentException("The DSN is invalid.");
        }
        parent::__construct($dsn, $username, $password, $options);
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    }
    
    // Prepare and execute an SQL statement
    public function executeQuery($sql, array $parameters = array()) {
        try {
            $this->statement = $this->prepare($sql);
            $this->statement->execute($parameters);
            $this->statement->setFetchMode(PDO::FETCH_OBJ);
            return $this->statement;
        }
        catch (PDOException $e) {
            throw new RunTimeException($e->getMessage());
        }
    }
    
    // Prepare and execute a SELECT statement
    public function select($table, array $bind = array(), $operator = "AND") {
        if ($bind) {
            $where = array();
            foreach ($bind as $col => $value) {
                unset($bind[$col]);
                $bind[":" . $col] = $value;
                $where[] = $col . " = :" . $col;
            }
        }
        $sql = "SELECT * FROM " . $table 
             . (($bind) ? " WHERE " 
             . implode(" " . $operator . " ", $where) : " ");
        return $this->executeQuery($sql, $bind);
    }
    
    // Prepare and execute an INSERT statement
    public function insert($table, array $bind) {
        $cols = implode(", ", array_keys($bind));
        $values = implode(", :", array_keys($bind));
        foreach ($bind as $col => $value) {
            unset($bind[$col]);
            $bind[":" . $col] = $value;
        }
        $sql = "INSERT INTO " . $table 
             . " (" . $cols . ")  VALUES (:" . $values . ")";
        $this->executeQuery($sql, $bind);
        return $this->lastInsertId();
    }
    
    // Prepare and execute an UPDATE statement
    public function update($table, array $bind, $where = "") {
        $set = array();
        foreach ($bind as $col => $value) {
            unset($bind[$col]);
            $bind[":" . $col] = $value;
            $set[] = $col . " = :" . $col;
        }
        $sql = "UPDATE " . $table . " SET " . implode(", ", $set) 
             . (($where) ? " WHERE " . $where : " ");
        return $this->executeQuery($sql, $bind)->rowCount();
    }
    
    // Prepare and execute a DELETE statement
    public function delete($table, $where = "") {
        $sql = "DELETE FROM " . $table 
             . (($where) ? " WHERE " . $where : " ");
        return $this->executeQuery($sql)->rowCount();
    }
}

The batch of tasks performed by the PdoAdapter class are indeed straightforward, limited to running a few prepared queries via its executeQuery() method and shooting some CRUD operations against a given table. The most relevant detail to highlight here is how the adapter overrides its parent’s constructor in order to do some checks and see if the PDO extension has been installed before starting consuming it.

PdoAdapter类执行的那批任务确实非常简单,仅限于通过其executeQuery()方法运行一些准备好的查询,并对给定的表进行一些CRUD操作。 这里要强调的最相关的细节是适配器如何覆盖其父级的构造函数,以便进行一些检查并查看在开始使用它之前是否已安装PDO扩展。

Here’s how we’d get the adapter up and running:

这是我们启动并运行适配器的方式:

<?php
use LibraryLoaderAutoloader,
    LibraryDatabasePdoAdapter;
  
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();
    
$adapter = new PdoAdapter("mysql:dbname=test", "myusername", "mypassword");
   
$stmt = $adapter->prepare("SELECT * FROM users");

$stmt->execute();

$users = $stmt->fetchAll(PDO::FETCH_OBJ);

foreach ($users as $user) {
    echo $user->name . " " . $user->email . "<br>";
}

I’m intentionally avoiding the adapter’s executeQuery() method and instead appealing to the native execute() method to pull in a few users from the database. At first blush this all seems to be pretty boring boilerplate stuff that should be skipped over for more interesting things… but bear with me because indeed there’s a tiny catch here worth stressing. Even considering that the adapter effectively implements a different constructor from the default one provided by PDO, the contract it agrees with client code is neatly maintained from top to bottom. In short, this means that having disparate constructor implementations in a base type and in a subtype by no means infringes the LSP substitutability rules as long as the contract exposed by the two classes is properly honored.

我故意避免使用适配器的executeQuery()方法,而是呼吁本机的execute()方法从数据库中引入一些用户。 乍一看,这一切似乎都是无聊的样板内容,应该跳过以获取更多有趣的内容……但请耐心等待,因为这里确实有一个小问题值得强调。 即使考虑到适配器有效地实现了与PDO提供的默认构造函数不同的构造函数,它与客户端代码一致的协定也始终由上至下维护。 简而言之,这意味着在基本类型和子类型中使用不同的构造函数实现绝不会侵犯LSP的可替换性规则,只要适当地遵守了这两个类所公开的契约即可。

Logically, this can be seen more clearly if we switch the previous script over to a native PDO instance rather than consuming the adapter:

从逻辑上讲,如果我们将先前的脚本切换到本地PDO实例而不是使用适配器,则可以更清楚地看到:

<?php
$adapter = new PDO("mysql:dbname=test", "myusername", "mypassword");

$stmt = $adapter->prepare("SELECT * FROM users");

$stmt->execute();

$users = $stmt->fetchAll(PDO::FETCH_OBJ);

foreach ($users as $user) {
    echo $user->name . " " . $user->email . "<br>";
}

The fact of having a hierarchy of classes, even a very elemental one, where the subtypes are constructed differently than the base types has literally nothing to do with breaking the desired substitutability that’s actively promoted by the LSP.

拥有一个类的层次结构,甚至是一个非常基本的类的事实,其子类型的构造与基本类型不同,这实际上与破坏LSP积极推动的所需可取代性无关。

You might argue that the signatures of the adapter and the native PDO look pretty much the same, and that’s why substitutability is in this case preserved so well. But it’s really easy to refute an argument like this. For instance, I could take a more pragmatic approach and refactor the adapter’s constructor so that it can accept an array of named connection arguments:

您可能会争辩说适配器和本机PDO的签名看起来几乎相同,这就是为什么在这种情况下可替代性保持得很好的原因。 但是,反驳这样的论点真的很容易。 例如,我可以采用一种更为实用的方法,并重构适配器的构造函数,以便它可以接受命名连接参数数组:

<?php
namespace LibraryDatabase;

class PdoAdapter extends PDO implements DatabaseAdapterInterface
{
    private $statement;   

    public function __construct(array $config = array()) {
        if (!extension_loaded("pdo")) {
            throw new InvalidArgumentException(
                "The adapter needs the PDO extension to be loaded.");
        }
        if (!isset($config["dsn"]) || !is_string($config["dsn"])) {
             throw new InvalidArgumentException(
                "The DSN has not been specified or is invalid.");
        }
        $username = $password = $options = null;
        extract($config);
        parent::__construct($dsn, $username, $password, $options);
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    }
    
    // the same implementation goes here
}

At this point the adapter’s constructor is a pretty different creature whose signature and implementation neatly accommodate an array of connection parameters instead of digesting plain scalars. With such a drastic refactoring in place, one might take for granted that isn’t possible to swap out at runtime an instance of the adapter with a native PDO one anymore. Rush judgments tend to be dangerous, something that the following snippet puts in evidence:

在这一点上,适配器的构造函数是一个完全不同的生物,其签名和实现巧妙地容纳了连接参数数组,而不是消化普通的标量。 有了如此激烈的重构,人们可能会认为,在运行时再也无法将适配器实例与本机PDO交换掉了。 匆忙的判断往往很危险,以下代码片段可以证明这一点:

<?php
$adapter = new PdoAdapter(array(
    "dsn"      => "mysql:dbname=test", 
    "username" => "myusername", 
    "password" => "mypassword"
));

$stmt = $adapter->prepare("SELECT * FROM users");

$stmt->execute();

$users = $stmt->fetchAll(PDO::FETCH_OBJ);

foreach ($users as $user) {
    echo $user->name . " " . $user->email . "<br>";
}

Even though instantiating an instance of the adapter is now quite a different process, the client code remains entirely agnostic about this change in the contract it consumes remains the same!

即使实例化适配器的实例现在是一个完全不同的过程,但是客户端代码仍然完全不了解其消耗的合同中的这种变化,并且保持不变!

Still, if you feel a pinch of skepticism and think the example falls short when it comes to demonstrating the interchangeability between types of a given hierarchy, look at the following which refactors even more drastically the constructor and supplies the connection arguments via a naive DTO (Data Transfer Object):

不过,如果您对此表示怀疑,并认为该示例在演示给定层次结构的类型之间的可互换性时不够用,请查看以下内容,该示例更加彻底地重构了构造函数,并通过幼稚的DTO提供了连接参数(数据传输对象):

<?php
namespace LibraryDatabase;

interface ConnectionDefinitionInterface
{
    public function getDsn();
    public function getUserName();
    public function getPassword();
    public function getOptions();
}
<?php
namespace LibraryDatabase;

class ConnectionDefinition implements ConnectionDefinitionInterface
{
    private $dsn;
    private $username;
    private $password;
    private $options = array();

    public function __construct($dsn, $username = null, $password = null, array $options = array()) {  
        if (!is_string($dsn) || empty($dsn)) {
            throw new InvalidArgumentException("The DSN is invalid.");
        }
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->options = $options;
    }
    
    public function getDsn() {
        return $this->dsn;
    }
    
    public function getUserName() {
        return $this->username;
    }
    
    public function getPassword() {
        return $this->password;
    }
    
    public function getOptions() {
        return $this->options;
    }
}
<?php
namespace LibraryDatabase;

class PdoAdapter extends PDO implements DatabaseAdapterInterface
{
    private $statement;   
    
    public function __construct(ConnectionDefinitionInterface $connectionDefinition) {
        if (!extension_loaded("pdo")) {
            throw new InvalidArgumentException(
                "The adapter needs the PDO extension to be loaded.");
        }
        parent::__construct(
            $connectionDefinition->getDsn(), 
            $connectionDefinition->getUserName(), 
            $connectionDefinition->getPassword(), 
            $connectionDefinition->getOptions()
        );
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    }

    // the same implementation goes here
}

While it’s fair to admit that DTOs don’t have a prolific existence in the PHP world (though some popular frameworks like Zend Framework 2 and Simfony 2 use them ubiquitously), in this case I appealed to a simple one to pass a set of connection arguments to the adapter’s constructor. The radical change, for obvious reasons, is going to ripple artifacts toward the code responsible for instantiating the adapter, be it a DIC, a low-level factory, or any other independent mechanism along that line that doesn’t interfere explicitly with application logic:

可以公平地承认DTO在PHP世界中并不存在(尽管某些流行的框架(如Zend Framework 2和Simfony 2普遍使用它们)),在这种情况下,我呼吁一个简单的传递一组连接适配器的构造函数的参数。 出于明显的原因,根本性的变化将使工件泛滥到负责实例化适配器的代码,它是DIC,低级工厂或沿这条线的,不会明显干扰应用程序逻辑的任何其他独立机制:

<?php
$adapter = new PdoAdapter(
    new ConnectionDefinition("mysql:dbname=test", "myusername", "mypassword")
);

As long as the adapter’s contract remains untouched along the way, it’s possible to interchange an instance of the adapter with a PDO one and client code won’t pitch a single compliant.

只要在此过程中未更改适配器的合同,就可以将适配器的实例与PDO互换,并且客户代码不会提出任何兼容要求。

Though this example might be somewhat overkill when it comes to pushing PDO functionality inside the boundaries of a slim wrapper, it does an excellent job at showing the flagrant LSP breakages that are easily found in the wild occur because of other reasons, which I dare to claim aren’t even related to the “flaws” of the base type/subtype construction schema.

尽管在将PDO功能推入超薄包装的边界内时,此示例可能有些过高,但它在显示由于其他原因而容易在野外容易发现的明显LSP破损方面表现出色,我敢于声明甚至与基本类型/子类型构造模式的“缺陷”无关。

结束语 (Closing Remarks)

The mantra has been said over and over, but it’s worth stressing it once again: providing a certain programming principle or pattern with a dogmatic connotation which must be blindly fulfilled is something almost as harmful as not using those patterns and principles at all, hence letting the chaos of pragmatism dramatically rot the quality of a system.

咒语已经一遍又一遍地讲了,但是值得再次强调:提供一定的编程原则或模式必须具有盲目的教条式含义,这与根本不使用这些模式和原理一样有害,因此,实用主义的混乱极大地腐蚀了系统的质量。

Aside from attempting to demystify constructors as one of the root causes of potential LSP infringements, my argument against including constructors in interfaces certainly isn’t meant to be dogma. But being aware of the consequences of this sort of “construction-restrictive” contracts just because PHP’s object model is relaxed in that aspect might help to mitigate the proliferation of this inflexible, pointless practice.

除了试图使构造函数神秘化是潜在的LSP侵权的根本原因之外,我反对在接口中包含构造函数的论点当然也不是教条。 但是仅由于PHP的对象模型在那方面有所放松而意识到此类“构造限制性”合同的后果,可能有助于减轻这种僵化,毫无意义的实践的泛滥。

Image via Fotolia

图片来自Fotolia

翻译自: https://www.sitepoint.com/constructors-and-the-myth-of-breaking-the-lsp/

liskov替换原则

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值