通过合同设计 (DBC) 违反里氏替换原则 (LSP)?

Liskov Substitution Principle (LSP) violated via Design By Contract (DBC)?

我正在 PHP 中编写一个框架,并且 运行 进入了一个难闻的模式。看来我正在实施违反里氏替换原则 (LSP) 的合同(q.v。按合同设计)。由于原始示例非常抽象,我将把它放在真实世界的上下文中:

(n.b。本人不是engine/vehicle/vroom-vroom人,不切实际请见谅)


假设我们有一个关于车辆的贫血抽象 class,并且我们还有两种子类型的车辆 - 可以加油的和不能加油的(例如电动自行车)。对于这个例子,我们将只关注可加油类型:

abstract class AbstractVehicle {}

abstract class AbstractFuelledVehicle extends AbstractVehicle
{
    private $lastRefuelPrice;

    final public function refuelVehicle(FuelInterface $fuel)
    {
        $this->checkFuelType($fuel);
        $this->lastRefuelPrice = $fuel->getCostPerLitre;
    }

    abstract protected function checkFuelType(FuelInterface $fuel);
}

abstract class AbstractNonFuelledVehicle extends AbstractVehicle { /* ... */ }

现在,让我们看看 "fuel" classes:

abstract class AbstractFuel implements FuelInterface
{
    private $costPerLitre;

    final public function __construct($costPerLitre)
    {
        $this->costPerLitre = $costPerLitre;
    }

    final public function getCostPerLitre()
    {
        return $this->costPerLitre;
    }
}

interface FuelInterface
{
    public function getCostPerLitre();
}

以上是抽象的class,现在让我们看看具体的实现。首先,fuel 的两个具体实现,包括一些贫血接口,以便我们可以 type-hint/sniff 正确地使用它们:

interface MotorVehicleFuelInterface {}

interface AviationFuelInterface {}

final class UnleadedPetrol extends AbstractFuel implements MotorVehicleFuelInterface {}

final class AvGas extends AbstractFuel implements AviationFuelInterface {}

现在终于有了车辆的具体实现,它确保使用正确的燃料类型(接口)为特定车辆加油 class,如果不兼容则抛出异常:

class Car extends AbstractFuelledVehicle
{
    final protected function checkFuelType(FuelInterface $fuel)
    {
        if(!($fuel instanceof MotorVehicleFuelInterface))
        {
            throw new Exception('You can only refuel a car with motor vehicle fuel');
        }
    }
}

class Jet extends AbstractFuelledVehicle
{
    final protected function checkFuelType(FuelInterface $fuel)
    {
        if(!($fuel instanceof AviationFuelInterface))
        {
            throw new Exception('You can only refuel a jet with aviation fuel');
        }
    }
}

Car和Jet都是AbstractFuelledVehicle的子类型,所以根据LSP,我们应该可以替代它们。

由于如果提供了错误的 AbstractFuel 子类型,checkFuelType() 会抛出异常,这意味着如果我们用 AbstractFuelledVehicle 子类型 Car 替换 Jet(反之亦然)如果不替换相关的燃料子类型,我们将触发异常。

这是:

  1. 明显违反 LSP,因为替换不应导致导致异常被抛出的行为改变
  2. 完全没有违规,因为接口和抽象函数都已正确实现并且仍然可以在没有类型违规的情况下调用
  3. 有点灰色地带,回答比较主观

转念一想,我认为这 在技术上违反了里氏替换原则。一种重新表述 LSP 的方式是“a subclass 应该只要求什么并且承诺不多”。在这种情况下,Car 和 Jet 混凝土 classes 都需要特定类型的燃料 才能继续执行代码 (这违反了 LSP),此外,方法 checkFuelType() 可以被覆盖以包含各种奇怪和奇妙的行为。我认为更好的方法是:


更改 AbstractFuelledVehicle class 以在提交加油前检查燃料类型:

abstract class AbstractFuelledVehicle extends AbstractVehicle
{
    private $lastRefuelPrice;

    final public function refuelVehicle(FuelInterface $fuel)
    {
        if($this->isFuelCompatible($fuel))
        {
            $this->lastRefuelPrice = $fuel->getCostPerLitre;
        } else {
            /* 
              Trigger some kind of warning here,
              whether externally via a message to the user
              or internally via an Exception
            */
        }
    }

    /** @return bool */
    abstract protected function isFuelCompatible(FuelInterface $fuel);
}

对我来说,这是一个更优雅的解决方案,并且没有任何代码味道。我们可以将燃料从 UnleadedPetrol 换成 AvGas,superclass 的行为保持不变,尽管有两种可能的结果(即它的行为是 而不是 由具体 class,可以抛出异常、记录错误、跳跳汰机等)

正在将评论合并为答案...

同意LSP的分析:原版是违规的,我们总是可以通过弱化层级顶端的合约来解决LSP违规问题。但是,我不会称这是一个优雅的解决方案。类型检查总是一种代码味道(在 OOP 中)。用 OP 自己的话来说,“...包括一些贫血界面,以便我们可以 type-hint/sniff 它们...”这里嗅到的是糟糕设计的恶臭。

我的观点是,LSP 在这里是最不关心的; instanceof 是一个 OO code smell。此处的 LSP 合规性就像是在腐烂的房子上刷新漆:它看起来很漂亮,但基础仍然不牢固。从设计中消除类型检查。才担心LSP。


一般的 OO 设计的 SOLID 原则,尤其是 LSP,作为设计的一部分是最有效的,实际上是面向对象的。在 OOP 中,类型检查被多态性所取代。