2016-08-30 1 views
0

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

(пь Я не двигатель/транспортное средство/Врум-Врум людей, простите меня, если это нереально)


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

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

автомобилей и Jet оба подтипа AbstractFuelledVehicle, поэтому в соответствии с LSP, мы должны быть в состоянии заменить их.

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

Является ли это:

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

Я не думаю, что это нарушение. – zerkms

+0

Я тоже так не думаю, но я хотел бы посмотреть, что думает сообщество. Там могут быть какие-то сторонние meeseeks :) –

+2

В этом случае договор на 'AbstractFuelledVehicle :: refuelVehicle()' есть: «Заправьте автомобиль, если топливо совместимо». Это не сломано. – zerkms

ответ

1

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

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

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


Твердые принципы проектирования ОО в целом, и, в частности, ЛСП, наиболее эффективен в качестве части конструкции, которая является, по сути, объектно-ориентированным. В ООП проверка типов заменяется полиморфизмом.

0

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


Alter класса 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); 
} 

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

+1

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

+0

Мы перекладываем ответ на несовместимое топливо в абстрактный (супер) класс, хотя это означает, что поведение будет одинаковым для всех подклассов. Разве это не то, что стремится LSP? LSP не говорит, что Исключения не могут быть выброшены, просто поведение подклассов должно отражать суперкласс. В этом случае договорное условие isFuelCompatible() заключается в том, что аргумент имеет тип FuelInterface, а постусловие - в том, что оно возвращает логическое значение. В отличие от абстрактного метода checkFuelType(), который имел такое же предварительное условие, но без постусловия. –

+0

Моя точка зрения заключается в том, что LSP здесь наименее опасен; 'instanceof' - это OO [запах кода] (https://www.google.com/#q=instanceof%20code%20smell). Соответствие LSP здесь похоже на свежую краску на гнилом доме: она может выглядеть красиво, но фундамент по-прежнему принципиально необоснован. Устраните проверку типа из конструкции. Только после этого беспокоиться о LSP. – jaco0646

Смежные вопросы