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

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

(н.б. Я не двигатель / транспортное средство / vroom-vroom человек, простите меня, если это нереально)


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

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 AbstractFuelledVehicle на Jet (или наоборот) не заменяя также соответствующий подтип топлива, мы вызовем исключение.

Это:

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

0

Решение

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

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

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


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

1

Другие решения

Если подумать, я верю в это является техническое нарушение принципа подстановки Лискова. Способ перефразировать LSP «подкласс не должен требовать больше ничего и обещать не меньше«. В этом случае классы автомобилей и бетон требуют определенного вида топлива для продолжения выполнения кода (это нарушение 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, и поведение суперкласса остается прежним, хотя и с двумя возможными результатами (то есть, это поведение не продиктовано конкретным классом, который может генерировать исключение, регистрировать ошибку, танцевать джиг и т. д.)

0

По вопросам рекламы [email protected]