Принцип замещения Лисков (LSP) нарушен с помощью Design By Contract (DBC)?

Я пишу фреймворк на PHP и столкнулся с шаблоном, который плохо пахнет. Похоже, что я реализую контракт (см. «Разработка по контракту»), который нарушает принцип замены Лискова (LSP). Поскольку исходный пример сильно абстрагирован, я помещу его в контекст реального мира:

(примечание: я не разбираюсь в двигателях/транспортных средствах/в-комнатах-в-комнатах, простите меня, если это нереально)


Предположим, у нас есть анемичный абстрактный класс для транспортных средств, и, кроме того, у нас есть два подтипа транспортных средств — те, которые можно заправлять, и те, которые нельзя (например, самокаты). В этом примере мы сосредоточимся только на перезаправляемом типе:

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, мы должны иметь возможность их заменить.

Из-за того, что checkFuelType() выдает исключение, если указан неверный подтип AbstractFuel, это означает, что если мы заменим Car на Jet (или наоборот) подтипа AbstractFuelledVehicle (или наоборот) без замены соответствующего топлива подтип, мы вызовем исключение.

Это:

  1. Определенное нарушение LSP, так как подстановка не должна вызывать изменения в поведении, приводящие к возникновению исключений.
  2. Это вообще не нарушение, так как все интерфейсы и абстрактные функции реализованы правильно и могут вызываться без нарушений типов.
  3. Немного серой зоны, чей ответ субъективен

person e_i_pi    schedule 30.08.2016    source источник
comment
Я не считаю это нарушением.   -  person zerkms    schedule 30.08.2016
comment
Я тоже так не думаю, но хотелось бы узнать, что думает сообщество. Там могут быть какие-то придирчивые meeseeks 'там :)   -  person e_i_pi    schedule 30.08.2016
comment
В этом случае контракт для AbstractFuelledVehicle::refuelVehicle() звучит так: Заправляйте транспортное средство, если топливо совместимо. Он не сломан.   -  person zerkms    schedule 30.08.2016


Ответы (2)


Объединение комментариев в ответ...

Я согласен с анализом LSP: исходная версия является нарушением, и мы всегда можем устранить нарушения LSP, ослабив контракт на вершине иерархии. Однако я бы не назвал это элегантным решением. Проверка типов — это всегда запах кода (в ООП). По словам самого ОП, «... в том числе некоторые анемичные интерфейсы, чтобы мы могли их печатать/обнюхивать...». То, что здесь обнюхивается, — это зловоние плохого дизайна.

Я хочу сказать, что LSP здесь вызывает наименьшее беспокойство; instanceof – это запах кода. Соблюдение LSP здесь похоже на свежую краску на гнилом доме: может и красиво, но фундамент по-прежнему несостоятелен. Исключите проверку типов из проекта. Только потом беспокойтесь о LSP.


Принципы объектно-ориентированного проектирования SOLID в целом и LSP в частности наиболее эффективны как часть проекта, который фактически является объектно-ориентированным. В ООП проверка типов заменена полиморфизмом.

person jaco0646    schedule 02.09.2016

Если подумать, я считаю, что это является техническим нарушением принципа подстановки Лискова. Можно перефразировать 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);
}

Для меня это гораздо более элегантное решение и не имеет никакого запаха кода. Мы можем заменить топливо с неэтилированного бензина на AvGas, и поведение суперкласса останется прежним, хотя и с двумя возможными последствиями (т. е. его поведение не диктуется ему конкретным классом, что может вызвать исключение , логировать ошибку, танцевать джигу и т.д.)

person e_i_pi    schedule 31.08.2016
comment
Я согласен с анализом LSP: исходная версия является нарушением, и мы всегда можем устранить нарушения LSP, ослабив контракт на вершине иерархии. Однако я бы не назвал это элегантным решением. Проверка типов — это всегда запах кода (в ООП). По словам самого ОП, ... в том числе некоторые анемичные интерфейсы, чтобы мы могли их печатать/нюхать... То, что здесь нюхается, - это запах плохого дизайна. - person jaco0646; 31.08.2016
comment
Однако мы переносим реакцию на несовместимое топливо в абстрактный (супер)класс, что означает, что поведение будет одинаковым для всех подклассов. Разве это не то, к чему стремится LSP? LSP не говорит, что нельзя генерировать исключения, просто поведение подклассов должно отражать поведение суперкласса. В этом случае договорным предварительным условием isFuelCompatible() является то, что аргумент имеет тип FuelInterface, а постусловием является то, что он возвращает логическое значение. В отличие от абстрактного метода checkFuelType(), у которого было такое же предусловие, но не было постусловия. - person e_i_pi; 01.09.2016
comment
Я хочу сказать, что LSP здесь вызывает наименьшее беспокойство; instanceof – это запах кода. Соблюдение LSP здесь похоже на свежую краску на гнилом доме: может и красиво, но фундамент по-прежнему несостоятелен. Исключите проверку типов из проекта. Только потом беспокойтесь о LSP. - person jaco0646; 02.09.2016
comment
Спасибо за комментарии. Вчера я много читал об этом, и это имеет сходство с проблемой круга-эллипса. Вы правы, instanceof — это запах кода, который указывает на нездоровый дизайн, я думаю, что это тот случай, когда нам нужно бросить вызов предпосылке (en.wikipedia.org/wiki/). Поместите это как ответ, объяснив, что Лисков спорен, когда основной дизайн ошибочен, я приму это как ответ. - person e_i_pi; 02.09.2016