里斯科夫替换

tech2023-11-20  40

里斯科夫替换

Welcome to the (overridden) Matrix. Hush… don’t tell anybody! In a deleted scene from the Matrix trilogy, the following dialogue takes place:

欢迎使用(替代)矩阵。 嘘……不要告诉任何人! 在从Matrix三部曲删除的场景中,发生以下对话:

Morpheus: Neo, I’m inside the Matrix right now. Sorry to give you the bad news but our agent-tracking PHP program needs a quick update. It currently uses PDO’s query() method with strings to fetch the status of all the Matrix agents from our database, but we need to do that with prepared queries instead.

Morpheus: Neo,我现在在Matrix中。 很抱歉给您带来坏消息,但我们的代理跟踪PHP程序需要快速更新。 当前,它使用带有字符串的PDO的query()方法从数据库中获取所有Matrix代理的状态,但是我们需要使用准备好的查询来完成。

Neo: Sounds fine, Morpheus. Can I get a copy of the program?

Neo:听起来不错,Morpheus。 我可以得到该程序的副本吗?

Morpheus: No problem. Just clone our repository and take a look at the AgentMapper.php and index.php files.

Morpheus:没问题。 只需克隆我们的存储库,然后查看AgentMapper.php和index.php文件。

Neo issues a few Git commands and soon the following code appear before his eyes.

Neo发布了一些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; // a PSR-0 compliant class loader 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>"; }

Neo: Morpheus, I just got the files. I’m going to subclass PDO and override its query() method so it can work with prepared queries. Because of my superhuman powers, I should be able to get this working in a snap. Keep calm.

Neo: Morpheus,我刚收到文件。 我将继承PDO并重写其query()方法,以便它可以与准备好的查询一起使用。 由于我超人的力量,我应该能够很快完成此工作。 保持冷静。

The smooth sound of a computer keyboard fills the air.

电脑键盘的柔和声音充斥着空气。

Neo: Morpheus, the subclass is ready to be tested. Feel free to check it out on your side.

Neo: Morpheus,该子类已准备好进行测试。 随时查看它在您身边。

Morpheus does a quick search on his laptop and sees the class below.

Morpheus在笔记本电脑上进行了快速搜索,然后看到下面的课程。

<?php namespace LibraryDatabase; class PdoAdapter extends PDO { protected $_statement; public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) { // check if a valid DSN has been passed in if (!is_string($dsn) || empty($dsn)) { throw new InvalidArgumentException("The DSN must be a non-empty string."); } try { // attempt to create a valid PDO object and set some attributes. 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()); } } }

Morpheus: The adapter looks good. I’ll give it a shot right away just to check if our agent mapper will be able to keep track of the active agents traveling across the Matrix. Wish me luck.

Morpheus:适配器看起来不错。 我将立即对其进行测试,以检查我们的代理映射器是否能够跟踪在矩阵中传播的活动代理。 祝我好运。

Morpheus hesitates for a moment and runs the previous index.php file, this time using Neo’s masterpiece PdoAdapter class.

Morpheus犹豫了一下,运行了之前的index.php文件,这次使用Neo的杰作PdoAdapter类。

And then, a scream!

然后,尖叫!

Morpheus: Neo, I’m sure you’re the One! It’s just that I got an awful fatal error on my face with the following message:

莫非斯:尼奥,我敢肯定你是那一个! 只是我的脸上出现了一个严重的致命错误,并显示以下消息:

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

Another scream.

再次尖叫。

Neo: What went wrong?! What went wrong?!

Neo:出了什么问题?! 什么地方出了错?!

More screams.

更多的尖叫声。

Morpheus: I really don’t know. Oh, Agent Smith is now coming for me!

莫菲斯:我真的不知道。 哦,史密斯探员现在来找我!

The communication suddenly goes off. A long, heavy silence wraps up the dialogue, suggesting that Morpheus got caught by surprise and was seriously injured by Agent Smith.

通讯突然中断。 漫长而沉重的沉寂结束了对话,这表明莫菲斯被惊讶地抓住了,并受到史密斯特工的严重伤害。

LSP不代表(L)azy,(S)illy(P)rgraphmers (LSP Doesn’t Stand for (L)azy, (S)illy (P)rogrammers)

Needless to say the dialog above is fictional, but the problem is unquestionably real. If Neo had learned only one or two things about the Liskov Substitution Principle (LSP) as the renowned hacker he used to be, Mr. Smith could have been traced in a jiffy. Best of all, Morpheus would have been saved from the agent’s evils intentions. What a pity for him, indeed.

毋庸置疑,以上对话是虚构的,但这个问题无疑是真实的。 如果Neo曾经作为一名著名的黑客,只了解过一次有关Liskov替代原理(LSP)的一两件事,那么史密斯先生就可以被轻易地追踪到。 最重要的是,莫非斯本可以摆脱特工的邪恶意图。 的确,对他来说可惜。

In many cases, however, PHP developers think about the LSP pretty much as Neo did before: LSP is nothing but a purist’s theoretical principle that has little or no application in practice. But they’re treading down the wrong path.

但是,在许多情况下,PHP开发人员对LSP的思考与Neo之前所做的非常相似:LSP只是纯粹主义者的理论原理,在实践中很少或根本没有应用。 但是他们正在走错路。

Even when the formal definition of the LSP makes eyes roll back (including mine), at its core it boils down to avoiding brittlely-defined class hierarchies where the descendants expose a behavior radically different from the base abstractions consuming the same contract.

即使LSP的正式定义使人回头(包括我的),它的核心还是可以避免避免使用脆弱定义的类层次结构,因为在这些层次结构中,后代暴露出的行为与使用同一合同的基本抽象完全不同。

In simple terms, the LSP establishes that when overriding a method in a subclass, it must fulfill the following requirements:

简而言之,LSP确立了在重写子类中的方法时,它必须满足以下要求:

Its signature must match that of its parent

其签名必须与其父代签名相符 Its preconditions (what to accept) must be the same or weaker

它的前提(接受什么)必须相同或更弱 Its post conditions (what to expect) must be the same or stronger

其职位条件(期望值)必须相同或更高 Exceptions (if any) must be of the same type than the ones thrown by its parent

异常(如果有)的类型必须与其父级抛出的类型相同

Now, feel free to reread the above list over again (don’t worry, I’ll wait), and you’ll hopefully realize why this makes a lot of sense.

现在,随时重新阅读上面的列表(不用担心,我会等待),您将希望知道为什么这样做很有意义。

Back to the example, Neo’s cardinal sin was simply not to keep method signatures the same, hence breaking the contract with client code. To fix up this issue, the agent mapper’s findAll() method could be rewritten with some conditionals (a clear sign of code smell), as shown below:

回到示例中,Neo的主要罪过就是不使方法签名保持相同,从而破坏了与客户代码的契约。 为了解决此问题,可以使用一些条件(代码气味的明显标志)重写代理映射器的findAll()方法,如下所示:

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

If you’re in a good mood and give the refactored method a try, it will work just fine, either when using a native PDO object or an instance of the PDO adapter. It may sound rough, I know, but this is only a quick and dirty fix which flagrantly violates the Opened/Closed principle.

如果您心情很好,请尝试使用重构方法,那么在使用本机PDO对象或PDO适配器实例时,它都可以正常工作。 我知道这听起来似乎很粗糙,但这只是一个快速而肮脏的修复程序,它公然违反了“ 打开/关闭”原则 。

On the other hand, it’s feasible to refactor the adapter’s query() method in order to match the signature of its overridden parent. But in doing so, all of the other conditions stated by the LSP should be fulfilled also. Simply put, this means that method overriding should be done with due caution, and only with strong, really strong, reasons.

另一方面,重构适配器的query()方法以匹配其重写父代的签名是可行的。 但是,这样做时,LSP规定的所有其他条件也应满足。 简而言之,这意味着方法重写应谨慎处理,并且要有充分的理由。

In many use cases, and assuming that it’s not possible to use Interfaces, it’s preferable to create subclasses that only extend (not override) the functionality of their base classes. In the case of Neo’s PDO adapter, this approach will function like a charm, and definitively won’t blow up client code at any level.

在许多用例中,并假设不可能使用Interfaces ,最好创建仅扩展(而不覆盖)其基类功能的子类。 在Neo的PDO适配器的情况下,这种方法将像魅力一样起作用,并且绝对不会在任何级别上破坏客户端代码。

As I just said, there’s a more efficient – yet radical – solution which appeals to the goodies of implementing interfaces. While the earlier PDO adapter was created via inheritance and admittedly broke the LSP commandments, the flaw comes actually from the way the agent mapper class was designed in the first place. Effectively, it relies from top to bottom on a concrete database adapter implementation, rather than on the contract defined by an interface. And the big OO powers say from ancient times that this is always a bad thing.

就像我刚才说的那样,有一个更有效但更激进的解决方案,它吸引了实现接口的好处。 虽然较早的PDO适配器是通过继承创建的,并且公然违反了LSP的规定,但该缺陷实际上出自于首先设计代理映射器类的方式。 实际上,它自上而下依赖于具体的数据库适配器实现,而不是依赖于接口定义的协定。 OO大国从远古时代就说这总是一件坏事。

So, how would the aforementioned solution be brought to life?

那么,上述解决方案将如何实现呢?

合同设计与反继承案 (Design by Contract and the Case against Inheritance)

Well, first off it’d be necessary to define a simple contract, which should be implemented later on by concrete database adapters. A trivial interface, like the one below, will do the trick nicely:

好吧,首先,有必要定义一个简单的协定,稍后应由具体的数据库适配器实现。 一个简单的接口(如下面的接口)将很好地完成此任务:

<?php namespace LibraryDatabase; interface DatabaseAdapterInterface { public function connect(); public function disconnect(); public function query($sql); }

So far, so good. With this interface up and running already, spawning a concrete database adapter is as simple as creating the following implementer:

到目前为止,一切都很好。 有了这个接口并开始运行,生成一个具体的数据库适配器就像创建以下实现程序一样简单:

<?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $_config = array(); protected $_connection; public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) { if (!is_string($dsn) || empty($dsn)) { throw new InvalidArgumentException("The DSN must be a non-empty string."); } // save connection parameters in the $_config field $this->_config = compact("dsn", "username", "password", "driverOptions"); } public function connect() { // if there is a PDO object already, return early if ($this->_connection) { return; } // otherwise try to create a PDO object try { $this->_connection = new PDO( $this->_config["dsn"], $this->_config["username"], $this->_config["password"], $this->_config["driverOptions"]); $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $this->_connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function disconnect() { $this->_connection = null; } public function query($sql, $fetchStyle = PDO::FETCH_OBJ) { $this->connect(); try { return $this->_connection->query($sql, $fetchStyle); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } }

Done. Even when the above class is pretty contrived, it adheres neatly to the contract that it implements. This allows for a bunch of SQL queries to be run without much struggle, and above all, without overriding anything relevant by mistake.

做完了 即使上面的类是非常人为设计的,它也会严格遵守其实现的合同。 这使得一堆SQL查询可以轻松运行,最重要的是不会错误地覆盖任何相关的查询。

The last change that must be made is to define an additional segregated interface, and refactor the initial mapper class so that the pitiless agents in the Matrix can be easily traced and beaten down in turn by injecting different database adapters at runtime.

最后要做的更改是定义一个额外的隔离接口 ,并重构初始的映射器类,以便可以通过在运行时注入不同的数据库适配器轻松地跟踪和淘汰Matrix中的无情代理。

That said, here’s how the aforementioned interface looks:

也就是说,这是上述界面的外观:

<?php namespace ModelMapper; interface AgentMapperInterface { public function findAll(); }

And here’s the revamped version of the agent mapper:

这是代理映射器的改进版本:

<?php namespace ModelMapper; use LibraryDataBaseDatabaseAdapterInterface; class AgentMapper implements AgentMapperInterface { protected $_adapter; protected $_table = "agents"; public function __construct(DatabaseAdapterInterface $adapter) { $this->_adapter = $adapter; } public function findAll() { try { return $this->_adapter->query("SELECT * FROM " . $this->_table); } catch (Exception $e) { return array(); } } }

Mission accomplished. Considering that now the refactored mapper expects to receive an implementer of the earlier database interface, it’s safe to say that its findAll() method won’t need to go into ugly checks to see what it got injected in the constructor. Definitively, we have a big winner here!

任务完成。 考虑到现在重构的映射器希望接收早期数据库接口的实现者,可以肯定地说,它的findAll()方法将不需要进行难看的检查来查看它在构造函数中注入了什么。 无疑,我们在这里有一个大赢家!

What’s more, the following code snippet shows how to put the previous elements to work side by side in sweet harmony:

此外,以下代码片段显示了如何使之前的元素以和谐的方式并排工作:

<?php use LibraryDatabasePdoAdapter, ModelMapperAgentMapper; // a PSR-0 compliant class loader require_once __DIR__ . "/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=Nebuchadnezzar", "morpheus", "aa26d7c557296a4e8d49b42c8615233a3443036d"); $agentMapper = new AgentMapper($adapter); $agents = $agentMapper->findAll(); foreach ($agents as $agent) { echo "Name: " . $agent->name . " - Status: " . $agent->status . "<br>"; }

Not bad at all, huh? The downside with this approach is that the whole refactoring process is way too drastic. And in more realistic use cases it won’t even be a viable solution (especially when dealing with large chunks of messy legacy code). Despite this, it shows in a nutshell how to create abstractions whose derivatives won’t break up the conditions imposed by the LSP, by using composition over inheritance, and with a pinch of Design by Contract also.

一点都不差吧? 这种方法的缺点是整个重构过程过于激烈。 而且在更实际的用例中,它甚至不是可行的解决方案(尤其是在处理大量凌乱的旧代码时)。 尽管如此,它还是概括性地展示了如何通过使用继承之上的构成以及少量的按合同设计来创建其派生物不会破坏LSP施加条件的抽象。

Of course, let’s not forget the morale of this story: the hard fight against mean machines that enjoy enslaving humans and using them like plain batteries will hopefully end up in a resounding triumph. In Neo we trust.

当然,让我们不要忘记这个故事的士气:与享受奴役人类并像普通电池一样使用它们的卑鄙机器进行艰苦的斗争,有望以惊人的胜利告终。 我们相信Neo。

The end.

结束。

闭幕词(矩阵外) (Closing Remarks (Outside the Matrix))

Being a central point of object-oriented design, and the “L” in the SOLID principles, the Liskov Substitution Principle has gained many angry detractors over the years, most likely because its academic definition is full of technical terms which blind sight and make hard to grasp what’s really behind its veil. Moreover, at face value it seems to contradict, or even condemn to an unavoidable doom of existence, of inheritance.

作为面向对象设计的中心点以及SOLID原则中的“ L”,Liskov替代原则多年来引起了许多愤怒的批评者,最有可能的原因是其学术定义充满了视力和难以理解的技术术语。掌握面纱背后的真正含义。 而且,从表面上看,它似乎与继承相矛盾,甚至谴责一种不可避免的生存厄运。

But this is just a misleading impression which vanishes into thin air as soon as one gets the principle’s actual meaning. At its core, LSP’s commitment is aimed at designing hierarchies of classes that expose a real IS-A relationship with each other, and where subclasses can be replaced by their base abstractions without spoiling the contract with client code.

但是,这只是一种误导性的印象,一旦人们理解了原理的实际含义就消失了。 LSP的核心宗旨是设计类的层次结构,以暴露彼此之间真正的IS-A关系,并且子类可以用其基本抽象替换,而不会破坏客户代码的约定。

So, make sure to stick to the principle’s commandments, and your life as a PHP developer (and hence your fat-wallet customers’) will be easier and way more enjoyable.

因此,请务必遵循该原则的诫命,您作为PHP开发人员的生活(以及您的胖​​钱包客户)将变得更加轻松愉快。

翻译自: https://www.sitepoint.com/liskov-substitution-principle/

里斯科夫替换

最新回复(0)