Liskov Substitution Principle 里氏替换原则
Introduction 介绍
Don't worry, the Liskov Substitution Principle is a lot easier to understand than it sounds. This principle states that you should be able to use any implementation of an abstraction in any place that accepts that abstraction. But, let's make this a little simpler. In plain English, the principle states that if a class uses an implementation of an interface, it must be able to use any implementation of that interface without requiring any modifications.
别担心,里氏替换原则读起来吓人学起来简单。该原则要求:一个抽象的任意一个实现,可以被用在任何需要该抽象的地方。读起来绕口,用普通人的话来解释一下。该原则规定:如果某处代码使用了一个接口的一个实现类,那么在这里也可以直接使用该接口的任何其他实现类,不用做出任何修改。
Liskov Substitution Principle 里氏替换原则
This principle states that objects should be replaceable with instances of their sub-types without altering the correctness of that program.
该原则规定对象应该可以被该对象子类的实例所替换,并且不会影响到程序的正确性。
In Action 实践
To illustrate this principle, let's continue to use our OrderProcessor
example from the previous chapter. Take a look at this method:
为了说明该原则,我们继续编写上一章节的OrderProcessor
。看下面的方法:
<!-- lang:php -->
public function process(Order $order)
{
// Validate order...
$this->orders->logOrder($order);
}
Note that after the Order
is validated, we log the order using the OrderReporsitoryInterface
implementation. Let's assume that when our order processing business was young, we stored all of our orders in CSV format on the file system. Our only OrderRepositoryInterface
implementation was a CsvOrderRepository
. Now, as our order rate grows, we want to use a relational database to store them. So, let's look at a possible implementation of our new repository:
注意当我们的Order
通过了验证,就被OrderRepositoryInterface
的实现对象存储起来了。假设当我们的业务刚起步时,我们将订单存储在CSV格式的文件系统中。我们的OrderRepositoryInterface
的实现类是CsvOrderRepository
。现在,随着我们订单增多,我们想用一个关系数据库来存储订单。那么我们来看看新的订单资料库类该怎么编写吧:
<!-- lang:php -->
class DatabaseOrderRepository implements OrderRepositoryInterface {
protected $connection;
public function connect($username, $password)
{
$this->connection = new DatabaseConnection($username, $password);
}
public function logOrder(Order $order)
{
$this->connection->run('insert into orders values (?, ?)', array(
$order->id, $order->amount
));
}
}
Now, let's examine how we would have to consume this implementation:
现在我们来研究如何使用这个实现类:
<!-- lang:php -->
public function process(Order $order)
{
// Validate order...
if($this->repository instanceof DatabaseOrderRepository)
{
$this->repository->connect('root', 'password');
}
$this->repository->logOrder($order);
}
Notice that we are forced to check if our OrderRepositoryInterface
is a database implementation from within our consuming processor class. If it is, we must connect to the database. This may not seem like a problem in a very small application, but what if the OrderRepositoryInterface
is consumed by dozens of other classes? We would be forced to implement this "bootstrap" code in every consumer. This will be a headache to maintain and is prone to bugs, and if we forgot to update a single consumer our application will break.
注意在这段代码中,我们必须在资料库外部检查OrderRepositoryInterface
的实例对象是不是用数据库实现的。如果是的话,则必须先连接数据库。在很小的应用中这可能不算什么问题,但如果OrderRepositoryInterface
被几十个类调用呢?我们可能就要把这段“启动”代码在每一个调用的地方复制一遍又一遍。这让人非常头疼难以维护,非常容易出错误。一旦我们忘了将所有调用的地方进行同步修改,那程序恐怕就会出问题。
The example above clearly breaks the Liskov Substitution Principle. We were unable to inject an implementation of our interface without changing the consumer to call the connect
method. So, now that we have identified the problem, let's fix it. Here is our new DatabaseOrderRepository
implementation:
很明显,上面的例子没有遵循里氏替换原则。如果不附加“启动”代码来调用connect
方法,则这段代码就没法用。好了,我们已经找到问题所在,咱们修好他。下面就是新的DatabaseOrderRepository
:
<!-- lang:php -->
class DatabaseOrderRepository implements OrderRepositoryInterface {
protected $connector;
public function __construct(DatabaseConnector $connector)
{
$this->connector = $connector;
}
public function connect()
{
return $this->connector->bootConnection();
}
public function logOrder(Order $order)
{
$connection = $this->connect();
$connection->run('insert into orders values (?, ?)', array(
$order->id, $order->amount
));
}
}
Now our DatabaseOrderRepository
is managing the connection to the database, and we can remove our "bootstrap" code from the consuming OrderProcessor
:
现在DatabaseOrderRepository
掌管了数据库连接,我们可以把“启动”代码从OrderProcessor
移除了:
<!-- lang:php -->
public function process(Order $order)
{
// Validate order...
$this->repository->logOrder($order);
}
With this modification, we can now use our CsvOrderRepository
or DatabaseOrderRepository
without modifying the OrderProcessor
consumer. Our code adheres to the Liskov Substitution Principle! Take note that many of the architecture concepts we have discussed are related to knowledge. Specifically, the knowledge a class has of its "surrounding", such as the peripheral code and dependencies that help a class do its job. As you work towards a robust application architecture, limiting class knowledge will be a recurring and important theme.
这样一改,我们就可以想用CsvOrderRepository
也行,想用DatabaseOrderRepository
也行,不用改OrderProcessor
一行代码。我们的代码终于实现了里氏替换原则!要注意,我们讨论过的许多架构概念都和_知识_相关。具体讲,知识就是一个类和它所具有的_周边领域_,比如用来帮助类完成任务的外围代码和依赖。当你要制作一个容错性强大的应用架构时,限制类的_知识_是一种常用且重要的手段。
Also note the consequence of violating the Liskov Substitution principle with regards to the other principle we have covered. By breaking this principle, the Open Closed principle must also be broken, as, if the consumer must check for instances of various child classes, this consumer must be changed each time there is a new child class.
还要注意如果不遵守里氏替换原则,那后果可能会影响到我们之前已经讨论过的其他原则。不遵守里氏替换原则,那么开放封闭原则一定也会被打破。因为,如果调用者必须检查实例属于哪个子类的,那一旦有个新的子类,调用者就得做出改变。(译者注:这就违背了对修改封闭的原则。)
Watch For Leaks 小心遗漏
You have probably noticed that this principle is closely related to the avoidance of "leaky abstractions", which were discussed in the previous chapter. Our database repository's leaky abstraction was our first clue that the Liskov Substitution Principle was being broken. Keep an eye out for those leaks!
你可能注意到这个原则和上一章节提到的“抽象的漏洞”密切相关。我们的数据库资料库的抽象漏洞就是没有遵守里氏替换原则的第一迹象。要留意那些漏洞!