里氏替换原则

简介

别担心,里氏替换原则名字起的高大上,但是其实很简单。该原则可以描述为:一个抽象的任意实现都可以在声明该抽象的地方替换它。读起来有点绕口,通俗点说就是:如果一个类使用了某个接口的实现,那么一定可以通过该接口的其它实现来替换它,不用做出任何修改。

里氏替换原则规定对象可以被其子类的实例所替换,并且不会影响到程序的正确性。

实战

为了说明该原则,我们继续使用前面编写的 OrderProcessor 类作为示例。请看下面的方法:

public function process(Order $order)
{
    // Validate order...
    $this->orders->logOrder($order);
}

注意,当 Order 通过验证后,我们就会通过 OrderRepositoryInterface 的实现类实例将其记录下来。假设订单处理业务刚起步时,我们将所有订单都存储到了 CSV 格式的文件系统中。对应的,我们的 OrderRepositoryInterface 的实现类就应该是CsvOrderRepository。现在,随着订单增多,我们想用一个关系数据库来存储订单。下面我们就来看看新的订单资料库类该怎么编写吧:

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
        ));
    }

}

现在,我们来研究如何使用这个实现类:

public function process(Order $order)
{
    // Validate order...

    if($this->repository instanceof DatabaseOrderRepository)
    {
        $this->repository->connect('root', 'password');
    }
    $this->repository->logOrder($order);
}

注意在这段代码中,我们不得不在调用的地方检查 OrderRepositoryInterface 接口是否是通过数据库实现的。如果是的话,则必须连接到数据库。在很小的应用中,这可能看起来没什么问题,但如果OrderRepositoryInterface 在很多类中被调用呢?我们可能就要把这段「启动」代码在每一个调用的地方重复实现。这让人非常头疼,不仅难以维护,而且非常容易出错误,并且一旦我们忘了将所有调用的地方进行同步修改,那程序恐怕就会出问题。

很明显,上面的例子违背了里氏替换原则。因为我们不能在不修改调用方代码的情况下注入接口的实现。所以,既然已经定位到问题所在,接下来就要修复它。下面就是新的 DatabaseOrderRepository 实现:

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 中移除了:

public function process(Order $order)
{
    // Validate order...

    $this->repository->logOrder($order);
}

这样一改,我们就可以在 CsvOrderRepositoryDatabaseOrderRepository 实现之间进行切换了,不用对 OrderProcessor 做任何修改。我们的代码终于实现了里氏替换原则!需要注意的是,我们讨论过的许多架构概念都和「知识」相关。具体来说,一个类所具备的「周边」知识,例如外围代码和依赖,会帮助这个类完成它的工作。当你想要构建一个健壮的大型应用时,限制类的知识会是一个反复出现、非常重要的主题。

还要注意如果不遵守里氏替换原则,那么可能会影响到我们之前已经讨论过的其他原则。不遵守里氏替换原则,那么开放封闭原则一定也会被打破。因为,如果调用者必须检查实例属于哪个子类,则一旦有了新的子类,调用者就得做出改变。

你可能已经注意到这个原则和前面提到的「泄露抽象实现细节」密切相关。数据库仓库类的实现细节泄露就是里氏替换原则被破坏的第一迹象。所以要时刻留意那些泄露!

上一篇: 开放封闭原则

下一篇: 接口隔离原则