Liskov替换原则(LSP)是否通过合同设计(DBC)违反?

时间:2016-08-30 04:04:43

标签: php oop solid-principles design-by-contract liskov-substitution-principle

我正在用PHP编写一个框架,并且遇到了一个闻起来很糟糕的模式。我似乎实施了违反Liskov替代原则(LSP)的合同(q.v. Design By Contract)。由于原始示例被大量抽象化,我将其置于现实世界的背景中:

(n.b。我不是引擎/车辆/扫帚 - 扫帚的人,请原谅我,如果它不切实际)

假设我们有车辆的贫血抽象类,我们还有两种子类型的车辆 - 可以加油的车辆和那些不能加油的车辆(例如推车)。对于这个例子,我们只关注可加燃料类型:

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 { /* ... */ }

现在,让我们来看看"燃料"类:

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

这就是完成的所有抽象类,现在让我们看一下具体的实现。首先,两个具体的燃料实现,包括一些贫血界面,以便我们可以正确地打字/嗅探它们:

interface MotorVehicleFuelInterface {}

interface AviationFuelInterface {}

final class UnleadedPetrol extends AbstractFuel implements MotorVehicleFuelInterface {}

final class AvGas extends AbstractFuel implements AviationFuelInterface {}

现在最后,我们有车辆的具体实施,确保正确的燃料类型(接口)用于加油特定的车辆类别,如果不相容则抛出异常:

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. 有点灰色区域,答案是主观的

2 个答案:

答案 0 :(得分:1)

将评论合并到答案中......

我同意LSP的分析:原始版本是违规的,我们总是可以通过弱化层次结构顶部的合同来解决LSP违规问题。但是,我不认为这是一个优雅的解决方案。类型检查始终是代码气味(在OOP中)。用OP自己的话说,“ ...包括一些贫血界面,以便我们可以打字/嗅探它们...... ”这里被嗅到的是糟糕设计的恶臭。

我的观点是LSP是最不重要的问题; instanceof是一个OO code smell。这里的LSP合规就像腐烂的房子上的新鲜油漆:它可能看起来很漂亮,但基础仍然根本不健全。从设计中消除类型检查。只有这样担心LSP。

一般而言,OO设计的SOLID原则,特别是LSP,作为实际上面向对象的设计的一部分是最有效的。在OOP中,类型检查由多态性替换。

答案 1 :(得分:0)

再想一想,我认为 是对Liskov替代原则的技术违反。一种重新定义LSP的方法是" 一个子类应该不再需要,并且承诺"。在这种情况下,Car和Jet混凝土类都需要特定类型的燃料才能继续执行代码(这违反了LSP),另外还有方法 checkFuelType ()可以被覆盖以包括各种奇怪和奇妙的行为。我认为更好的方法是:

更改AbstractFuelledVehicle类以在加油前检查燃油类型:

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,并且超类的行为保持不变,尽管有两种可能的结果(即它的行为由具体类决定它,可以抛出异常,记录错误,跳过跳汰机等等)