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。看下面的方法:

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。现在,随着我们订单增多,我们想用一个关系数据库来存储订单。那么我们来看看新的订单资料库类该怎么编写吧:

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:

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

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:

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

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!

你可能注意到这个原则和上一章节提到的“抽象的漏洞”密切相关。我们的数据库资料库的抽象漏洞就是没有遵守里氏替换原则的第一迹象。要留意那些漏洞!

results matching ""

    No results matching ""