### 简介
别担心,里氏替换原则名字起的高大上,但是其实很简单。该原则可以描述为:一个抽象的任意实现都可以在声明该抽象的地方替换它。读起来有点绕口,通俗点说就是:如果一个类使用了某个接口的实现,那么一定可以通过该接口的其它实现来替换它,不用做出任何修改。
> 里氏替换原则规定对象可以被其子类的实例所替换,并且不会影响到程序的正确性。
### 实战
为了说明该原则,我们继续使用前面编写的 `OrderProcessor` 类作为示例。请看下面的方法:
```php
public function process(Order $order)
{
// Validate order...
$this->orders->logOrder($order);
}
```
注意,当 `Order` 通过验证后,我们就会通过 `OrderRepositoryInterface` 的实现类实例将其记录下来。假设订单处理业务刚起步时,我们将所有订单都存储到了 CSV 格式的文件系统中。对应的,我们的 `OrderRepositoryInterface` 的实现类就应该是`CsvOrderRepository`。现在,随着订单增多,我们想用一个关系数据库来存储订单。下面我们就来看看新的订单资料库类该怎么编写吧:
```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
));
}
}
```
现在,我们来研究如何使用这个实现类:
```php
public function process(Order $order)
{
// Validate order...
if($this->repository instanceof DatabaseOrderRepository)
{
$this->repository->connect('root', 'password');
}
$this->repository->logOrder($order);
}
```
注意在这段代码中,我们不得不在调用的地方检查 `OrderRepositoryInterface` 接口是否是通过数据库实现的。如果是的话,则必须连接到数据库。在很小的应用中,这可能看起来没什么问题,但如果`OrderRepositoryInterface` 在很多类中被调用呢?我们可能就要把这段「启动」代码在每一个调用的地方重复实现。这让人非常头疼,不仅难以维护,而且非常容易出错误,并且一旦我们忘了将所有调用的地方进行同步修改,那程序恐怕就会出问题。
很明显,上面的例子违背了里氏替换原则。因为我们不能在不修改调用方代码的情况下注入接口的实现。所以,既然已经定位到问题所在,接下来就要修复它。下面就是新的 `DatabaseOrderRepository` 实现:
```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
));
}
}
```
现在 `DatabaseOrderRepository` 自己接管了数据库连接,这样我们就可以把数据库「启动」代码从 `OrderProcessor` 中移除了:
```php
public function process(Order $order)
{
// Validate order...
$this->repository->logOrder($order);
}
```
这样一改,我们就可以在 `CsvOrderRepository` 和 `DatabaseOrderRepository` 实现之间进行切换了,不用对 `OrderProcessor` 做任何修改。我们的代码终于实现了里氏替换原则!需要注意的是,我们讨论过的许多架构概念都和「知识」相关。具体来说,一个类所具备的「周边」知识,例如外围代码和依赖,会帮助这个类完成它的工作。当你想要构建一个健壮的大型应用时,限制类的知识会是一个反复出现、非常重要的主题。
还要注意如果不遵守里氏替换原则,那么可能会影响到我们之前已经讨论过的其他原则。不遵守里氏替换原则,那么开放封闭原则一定也会被打破。因为,如果调用者必须检查实例属于哪个子类,则一旦有了新的子类,调用者就得做出改变。
> 你可能已经注意到这个原则和前面提到的「泄露抽象实现细节」密切相关。数据库仓库类的实现细节泄露就是里氏替换原则被破坏的第一迹象。所以要时刻留意那些泄露!