Что является примером принципа подстановки Лискова?

Я слышал, что принцип замещения Лискова (LSP) является фундаментальным принципом объектно-ориентированного проектирования. Что это такое и каковы некоторые примеры его использования?

Больше примеров присоединения и нарушения LSP здесь
StuartLC 15.05.2015 13:20:02
Этот вопрос имеет бесконечно много хороших ответов и поэтому слишком широк .
Raedwald 16.12.2018 15:30:38
30 ОТВЕТОВ

Будет ли реализация ThreeDBoard с точки зрения массива Board настолько полезной?

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

Что касается внешнего интерфейса, вы можете выделить интерфейс Board для TwoDBoard и ThreeDBoard (хотя ни один из вышеперечисленных методов не подходит).

4
11.09.2008 15:28:44
Я думаю, что этот пример просто демонстрирует, что наследование от платы не имеет смысла в контексте ThreeDBoard, и все сигнатуры методов не имеют смысла с осью Z.
NotMyself 11.09.2008 15:32:38
РЕШЕНИЕ

Принцип замещения Лискова (LSP, ) - это концепция объектно-ориентированного программирования, которая гласит:

Функции, которые используют указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

По сути, LSP - это интерфейсы и контракты, а также то, как решать, когда расширять класс, а не использовать другую стратегию, например композицию, для достижения своей цели.

Наиболее эффективный способ проиллюстрировать этот момент - « Head First OOA & D» . Они представляют сценарий, в котором вы являетесь разработчиком проекта по созданию платформы для стратегических игр.

Они представляют класс, представляющий доску, которая выглядит следующим образом:

Диаграмма классов

Все методы принимают координаты X и Y в качестве параметров для определения положения плитки в двумерном массиве Tiles. Это позволит разработчику игры управлять юнитами на игровом поле в течение игры.

В книге далее изменяются требования, чтобы сказать, что структура игры должна также поддерживать 3D игровые поля, чтобы приспособиться к играм, в которых есть полет. Итак ThreeDBoard, введен класс, который расширяется Board.

На первый взгляд это кажется хорошим решением. Boardобеспечивает как Heightи Widthсвойства и ThreeDBoardобеспечивает ось Z.

Где это ломается, когда вы смотрите на всех других членов, унаследованных от Board. Методы AddUnit, GetTile, GetUnitsи так далее, все принимать как X и Y параметры в Boardклассе , но ThreeDBoardнужен параметр Z , а также.

Таким образом, вы должны снова реализовать эти методы с параметром Z. Параметр Z не имеет контекста для Boardкласса, а унаследованные от Boardкласса методы теряют свое значение. Единице кода, пытающейся использовать ThreeDBoardкласс в качестве базового класса Board, будет очень не повезло.

Может быть, мы должны найти другой подход. Вместо того, чтобы расширяться Board, ThreeDBoardдолжен состоять из Boardобъектов. Один Boardобъект на единицу оси Z.

Это позволяет нам использовать хорошие объектно-ориентированные принципы, такие как инкапсуляция и повторное использование, и не нарушает LSP.

480
8.02.2017 14:07:47
См. Также Задача о круговом эллипсе в Википедии, чтобы найти аналогичный, но более простой пример.
Brian 21.10.2011 17:55:13
Requote от @NotMySelf: «Я думаю, что пример - просто продемонстрировать, что наследование от платы не имеет смысла в контексте ThreeDBoard, и все сигнатуры методов не имеют смысла с осью Z».
Contango 5.06.2013 16:40:15
Так что, если мы добавим еще один метод в класс Child, но все функции Parent все еще имеют смысл в классе Child, это нарушит LSP? Поскольку, с одной стороны, мы немного изменили интерфейс для использования дочернего элемента, с другой стороны, если мы приведем дочерний элемент к родительскому типу, код, ожидающий, что родительский элемент будет работать нормально.
Nickolay Kondratyev 18.06.2013 16:45:41
Это антилисковский пример. Лисков заставляет нас выводить прямоугольник с площади. Класс больше параметров из класса меньше параметров. И вы хорошо показали, что это плохо. Это действительно хорошая шутка, чтобы пометить в качестве ответа и получить 200 голосов за антилисовский ответ на вопрос о Лискове. Действительно ли принцип Лискова ошибочен?
Gangnus 18.10.2015 08:40:51
Я видел, как наследование работает неправильно. Вот пример. Базовый класс должен быть 3DBoard и производным классом Board. У доски все еще есть ось Z Макс (Z) = Мин (Z) = 1
Paulustrious 5.08.2017 14:52:03

Функции, которые используют указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Когда я впервые прочитал о LSP, я предположил, что это подразумевается в очень строгом смысле, по сути приравнивая его к реализации интерфейса и приведению типов к типу. Что означало бы, что LSP обеспечивается или не обеспечивается самим языком. Например, в этом строгом смысле ThreeDBoard, безусловно, заменяет Board в том, что касается компилятора.

После прочтения более подробно о концепции, я обнаружил, что LSP обычно интерпретируется более широко, чем это.

Короче говоря, то, что означает для клиентского кода «знать», что объект за указателем имеет производный тип, а не тип указателя, не ограничивается безопасностью типов. Приверженность LSP также может быть проверена путем проверки фактического поведения объектов. Таким образом, исследуя влияние состояния объекта и аргументов метода на результаты вызовов метода или типы исключений, выбрасываемых из объекта.

Возвращаясь к примеру снова, теоретически можно заставить методы Board работать на ThreeDBoard просто отлично. На практике, однако, будет очень трудно предотвратить различия в поведении, которые клиент может не обработать должным образом, не ограничивая функциональность, которую ThreeDBoard намеревается добавить.

Имея эти знания в руках, оценка соблюдения LSP может быть отличным инструментом для определения того, когда состав является более подходящим механизмом для расширения существующей функциональности, а не наследования.

20
7.10.2009 02:44:33

У Роберта Мартина есть отличная статья о принципе замены Лискова . В нем обсуждаются тонкие и не очень тонкие способы нарушения принципа.

Некоторые важные части документа (обратите внимание, что второй пример сильно сжат):

Простой пример нарушения LSP

Одним из наиболее вопиющих нарушений этого принципа является использование информации о типе среды выполнения C ++ (RTTI) для выбора функции в зависимости от типа объекта. то есть:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Понятно, что DrawShapeфункция плохо сформирована. Он должен знать о каждой возможной производной Shapeкласса и должен изменяться всякий раз, когда Shapeсоздаются новые производные . Действительно, многие рассматривают структуру этой функции как анафему для объектно-ориентированного дизайна.

Квадрат и прямоугольник, более тонкое нарушение.

Однако есть и другие, гораздо более тонкие способы нарушения LSP. Рассмотрим приложение, которое использует Rectangleкласс, как описано ниже:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Представьте, что однажды пользователи требуют умения манипулировать квадратами в дополнение к прямоугольникам. [...]

Ясно, что квадрат - это прямоугольник для всех нормальных намерений и целей. Поскольку отношения ISA сохраняются, логично смоделировать Square класс как производный от Rectangle. [...]

Squareунаследует SetWidthи SetHeightфункции. Эти функции совершенно неуместны Square, поскольку ширина и высота квадрата одинаковы. Это должно быть существенным признаком того, что существует проблема с дизайном. Однако есть способ обойти проблему. Мы могли бы переопределить SetWidthи SetHeight[...]

Но рассмотрим следующую функцию:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Если мы передадим ссылку на Squareобъект в эту функцию, Squareобъект будет поврежден, потому что высота не будет изменена. Это явное нарушение ЛСП. Функция не работает для производных своих аргументов.

[...]

77
16.08.2017 09:35:04
Поздно, но я подумал, что в этой статье была интересная цитата: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. если предварительное условие дочернего класса сильнее предварительного условия родительского класса, вы не можете заменить дочернего элемента родителем, не нарушив этого предварительного условия. Отсюда и LSP.
user2023861 11.02.2015 15:31:17
@ user2023861 Вы совершенно правы. Я напишу ответ на основании этого.
inf3rno 9.10.2017 04:00:54

LSP касается инвариантов.

Классический пример дается следующим объявлением псевдокода (реализации опущены):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

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

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Однако этот инвариант должен быть нарушен правильной реализацией Square, поэтому он не является допустимой заменой Rectangle.

130
6.05.2018 11:30:48
И, следовательно, сложность использования «ОО» для моделирования всего, что мы могли бы захотеть на самом деле моделировать.
DrPizza 30.11.2009 06:54:01
@DrPizza: Абсолютно. Однако две вещи. Во-первых, такие отношения все еще можно смоделировать в ООП, хотя и не полностью или с использованием более сложных обходных путей (выберите тот, который подходит вашей проблеме). Во-вторых, лучшей альтернативы нет. Другие сопоставления / моделирования имеют те же или похожие проблемы. ;-)
Konrad Rudolph 30.11.2009 09:11:01
@NickW В некоторых случаях (но не в приведенном выше) вы можете просто инвертировать цепочку наследования - логически говоря, 2D-точка - это 3D-точка, в которой не учитывается третье измерение (или 0 - все точки лежат в одной плоскости в 3D пространство). Но это, конечно, не очень практично. В общем, это один из случаев, когда наследование на самом деле не помогает, и между сущностями нет естественных отношений. Смоделируйте их отдельно (по крайней мере, я не знаю лучшего способа).
Konrad Rudolph 24.01.2012 09:49:26
ООП предназначен для моделирования поведения, а не данных. Ваши занятия нарушают инкапсуляцию еще до нарушения LSP.
Sklivvz 19.05.2012 21:47:45
@AustinWBryan Yep; Чем дольше я работаю в этой области, тем больше я склонен использовать наследование только для интерфейсов и абстрактных базовых классов, а для остальных - композицию. Иногда это немного больше работы (печатать мудро), но это позволяет избежать целого ряда проблем и широко повторяется советами других опытных программистов.
Konrad Rudolph 9.05.2018 16:37:39

LSP - это правило о договоре условий: если базовый класс удовлетворяет договору, то производные классы LSP также должны удовлетворять этому договору.

В псевдо-питоне

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

удовлетворяет LSP, если каждый раз, когда вы вызываете Foo для объекта Derived, он дает те же результаты, что и вызов Foo для объекта Base, при условии, что аргумент arg одинаков.

21
8.11.2008 17:53:32
Но ... если вы всегда получаете одно и то же поведение, то какой смысл иметь производный класс?
Leonid 3.07.2012 17:14:27
Вы упустили момент: это то же самое наблюдаемое поведение. Вы можете, например, заменить что-то с производительностью O (n) на что-то функционально эквивалентное, но с производительностью O (lg n). Или вы можете заменить что-то, что обращается к данным, реализованным с помощью MySQL, и заменить это базой данных в памяти.
Charlie Martin 4.07.2012 18:06:52
@Charlie Martin, кодирование интерфейса, а не реализации - я копаю это. Это не уникально для ООП; функциональные языки, такие как Clojure, также способствуют этому. Даже с точки зрения Java или C #, я думаю, что использование интерфейса вместо абстрактного класса плюс иерархии классов было бы естественным для примеров, которые вы предоставляете. Python не является строго типизированным и на самом деле не имеет интерфейсов, по крайней мере, явно. Моя трудность в том, что я несколько лет занимаюсь ООП, не придерживаясь SOLID. Теперь, когда я столкнулся с этим, это кажется ограничивающим и почти противоречивым.
Hamish Grubijan 6.07.2012 18:09:58
Ну, тебе нужно вернуться и проверить оригинальную статью Барбары. reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Это на самом деле не указано в терминах интерфейсов, и это логическое отношение, которое сохраняется (или не имеет) в любом язык программирования, который имеет некоторую форму наследования.
Charlie Martin 7.07.2012 19:22:07
@HamishGrubijan Я не знаю, кто сказал вам, что Python не слишком типизирован, но они лгали вам (и если вы мне не верите, запустите интерпретатор Python и попробуйте 2 + "2"). Возможно, вы путаете «строго типизированный» со «статически типизированным»?
asmeurer 20.01.2013 06:19:06

Отличным примером, иллюстрирующим LSP (предоставленный дядей Бобом в подкасте, который я недавно слышал), было то, как иногда что-то, что звучит правильно на естественном языке, не совсем работает в коде.

В математике а Squareесть Rectangle. На самом деле это специализация прямоугольника. «Is» заставляет вас моделировать это с наследованием. Однако, если в коде, который вы сделали, Squareпроисходит от Rectangle, то a Squareдолжен использоваться везде, где вы ожидаете a Rectangle. Это делает для некоторого странного поведения.

Представьте, что у вас есть SetWidthи SetHeightметоды в вашем Rectangleбазовом классе; это кажется совершенно логичным. Однако, если ваша Rectangleссылка указывает на a Square, тогда SetWidthи SetHeightне имеет смысла, потому что установка одного изменит другой, чтобы соответствовать ему. В этом случае Squareне проходит Лисковский тест с заменой, Rectangleи абстракция наличия Squareнаследования Rectangleявляется плохой.

введите описание изображения здесь

Вы должны проверить другие бесценные мотивационные плакаты Принципов ТВЕРДЫХ .

876
25.05.2019 09:27:38
@ m-sharp Что если это неизменный прямоугольник, такой, что вместо SetWidth и SetHeight у нас есть методы GetWidth и GetHeight?
Pacerier 26.04.2012 19:28:38
Мораль истории: смоделируйте свои занятия на основе поведения, а не свойств; смоделируйте свои данные на основе свойств, а не на поведении. Если он ведет себя как утка, это, безусловно, птица.
Sklivvz 19.05.2012 21:43:28
Ну, квадрат явно является типом прямоугольника в реальном мире. Можем ли мы смоделировать это в нашем коде, зависит от спецификации. LSP указывает, что поведение подтипа должно соответствовать поведению базового типа, как определено в спецификации базового типа. Если спецификация базового типа прямоугольника говорит, что высота и ширина могут быть установлены независимо, то LSP говорит, что квадрат не может быть подтипом прямоугольника. Если спецификация прямоугольника говорит, что прямоугольник является неизменным, то квадрат может быть подтипом прямоугольника. Все дело в подтипах, поддерживающих поведение, указанное для базового типа.
SteveT 24.09.2012 15:46:25
@Pacerier нет проблем, если он неизменен. Реальная проблема здесь заключается в том, что мы моделируем не прямоугольники, а скорее «изменяемые прямоугольники», то есть прямоугольники, ширину или высоту которых можно изменить после создания (и мы по-прежнему считаем, что это один и тот же объект). Если мы посмотрим на класс прямоугольника таким образом, то станет ясно, что квадрат не является «прямоугольником с изменяемой формой», потому что квадрат не может быть изменен и все же будет квадратом (в общем). Математически мы не видим проблемы, потому что изменчивость даже не имеет смысла в математическом контексте.
asmeurer 20.01.2013 06:13:21
У меня есть один вопрос о принципе. Почему бы проблема , если Square.setWidth(int width)была реализована следующим образом: this.width = width; this.height = width;? В этом случае гарантируется, что ширина равна высоте.
MC Emperor 28.10.2015 00:45:59

Эта формулировка LSP слишком сильна:

Если для каждого объекта o1 типа S существует объект o2 типа T такой, что для всех программ P, определенных в терминах T, поведение P не изменяется, когда o1 заменяется на o2, тогда S является подтипом T.

Что в основном означает, что S - это другая, полностью инкапсулированная реализация той же вещи, что и T. И я мог бы быть смелым и решить, что производительность является частью поведения P ...

Так что, в принципе, любое использование позднего связывания нарушает LSP. Весь смысл ОО в том, чтобы получить другое поведение, когда мы заменяем объект одного вида другим!

Формулировка, на которую ссылается Википедия , лучше, поскольку свойство зависит от контекста и не обязательно включает в себя все поведение программы.

9
3.04.2009 00:11:54
Хм, эта формулировка принадлежит Барбаре Лисков. Барбара Лисков, «Абстракция данных и иерархия», SIGPLAN Notices, 23,5 (май 1988 г.). Оно не «слишком сильное», оно «совершенно правильное» и не имеет значения, которое, по вашему мнению, имеет. Он сильный, но обладает достаточным количеством силы.
DrPizza 30.11.2009 07:08:42
Тогда в реальной жизни очень мало подтипов :)
Damien Pollet 8.12.2009 22:26:32
«Поведение не изменяется» не означает, что подтип даст вам точно такое же конкретное значение (и) результата. Это означает, что поведение подтипа соответствует ожидаемому в базовом типе. Пример: базовый тип Shape может иметь метод draw () и предусматривать, что этот метод должен отображать форму. Два подтипа Shape (например, Square и Circle) будут реализовывать метод draw (), и результаты будут выглядеть по-разному. Но до тех пор, пока поведение (рендеринг фигуры) соответствует указанному поведению Shape, тогда Square и Circle будут подтипами Shape в соответствии с LSP.
SteveT 11.10.2012 19:38:45

LSP необходим, когда некоторый код думает, что он вызывает методы типа T, и может неосознанно вызывать методы типа S, где S extends T(то есть Sнаследует, наследует или является подтипом супертипа T).

Например, это происходит, когда функция с входным параметром типа Tвызывается (то есть вызывается) со значением аргумента типа S. Или, где идентификатор типа T, присваивается значение типа S.

val id : T = new S() // id thinks it's a T, but is a S

LSP требует, чтобы ожидания (то есть инварианты) для методов типа T(например Rectangle) не нарушались при вызове методов типа S(например Square).

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Даже у типа с неизменяемыми полями все еще есть инварианты, например неизменяемые установщики Rectangle ожидают, что размеры будут изменены независимо, но неизменные установщики Square нарушают это ожидание.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP требует, чтобы у каждого метода подтипа Sбыли контравариантные входные параметры и ковариантный выход.

Контравариантный означает, что дисперсия противоречит направлению наследования, то есть тип Siкаждого входного параметра каждого метода подтипа Sдолжен быть одинаковым или супертипом типа Tiсоответствующего входного параметра соответствующего метода супертипа. T,

Ковариантность означает, что дисперсия находится в том же направлении наследования, то есть тип Soвыходных данных каждого метода подтипа Sдолжен быть одинаковым или подтипом типа Toсоответствующего выходного сигнала соответствующего метода супертипа T.

Это происходит потому, что если вызывающий объект Tдумает, что у него есть тип , что он вызывает метод T, то он предоставляет аргумент (ы) типа Tiи присваивает вывод типу To. Когда он фактически вызывает соответствующий метод S, каждый Tiвходной аргумент назначается Siвходному параметру, а Soвыходной - типу To. Таким образом , если Siне контравариантен WRT к Ti, то подтип Xi-Какой не будет подтипом Si-Не могли бы быть назначены Ti.

Кроме того, для языков (например, Scala или Ceylon), которые имеют аннотации отклонений на месте определения параметров полиморфизма типов (т. Е. Дженерики), совместное или обратное направление аннотации дисперсии для каждого параметра типа этого типа Tдолжно быть противоположным или одинаковым направлением. соответственно каждому входному параметру или выходу (каждого метода T), который имеет тип параметра типа.

Кроме того, для каждого входного параметра или выхода, который имеет тип функции, требуемое направление отклонения меняется на противоположное. Это правило применяется рекурсивно.


Подтип подходит для тех случаев, когда можно перечислить инварианты.

В настоящее время проводится много исследований о том, как моделировать инварианты, чтобы они обеспечивались компилятором.

Typestate (см. Стр. 3) объявляет и применяет инварианты состояния, ортогональные типу. Альтернативно, инварианты могут быть реализованы путем преобразования утверждений в типы . Например, чтобы утверждать, что файл открыт до его закрытия, File.open () может вернуть тип OpenFile, который содержит метод close (), недоступный в File. Крестики-нолики API , может быть еще одним примером применения печатать для обеспечения инвариантов во время компиляции. Система типов может быть даже полной по Тьюрингу, например, Scala . Языки с независимой типизацией и доказатели теорем формализуют модели типизации высшего порядка.

Из-за необходимости семантики абстрагироваться от расширения , я ожидаю, что использование типизации для моделирования инвариантов, то есть унифицированной денотационной семантики высшего порядка, превосходит Typestate. «Расширение» означает неограниченную перестановочную композицию несогласованного модульного развития. Поскольку мне кажется, что антитеза объединения и, следовательно, степеней свободы, иметь две взаимозависимые модели (например, типы и Typestate) для выражения общей семантики, которые не могут быть объединены друг с другом для расширяемой композиции. , Например, похожее на выражение выражение расширение было унифицировано в областях подтипирования, перегрузки функций и параметрической типизации.

Моя теоретическая позиция заключается в том, что для существования знаний (см. Раздел «Централизация слепа и непригодна») никогда не будет общей модели, которая могла бы обеспечить 100% -ное покрытие всех возможных инвариантов на языке Тьюринга. Чтобы знания существовали, неожиданных возможностей много, то есть беспорядок и энтропия всегда должны увеличиваться. Это энтропийная сила. Чтобы доказать все возможные вычисления потенциального расширения, нужно вычислить априори все возможные расширения.

Вот почему существует теорема Остановки, т. Е. Неразрешимо, завершается ли каждая возможная программа на языке программирования, полного по Тьюрингу. Можно доказать, что какая-то конкретная программа завершается (та, для которой все возможности определены и вычислены). Но невозможно доказать, что все возможные расширения этой программы прекращаются, если только возможности расширения этой программы не являются полными по Тьюрингу (например, через зависимую типизацию). Поскольку основным требованием для полноты по Тьюрингу является неограниченная рекурсия , интуитивно понятно, как теоремы Гёделя о неполноте и парадокс Рассела применимы к расширению.

Интерпретация этих теорем включает их в обобщенное концептуальное понимание энтропийной силы:

  • Теоремы Гёделя о неполноте : любая формальная теория, в которой все арифметические истины могут быть доказаны, противоречива.
  • Парадокс Рассела : каждое правило членства для набора, которое может содержать набор, перечисляет конкретный тип каждого члена или содержит себя. Таким образом, множества либо не могут быть расширены, либо являются неограниченной рекурсией. Например, набор всего, что не является чайником, включает в себя, включает в себя, включает в себя и т. Д. Таким образом, правило является непоследовательным, если оно (может содержать набор и) не перечисляет конкретные типы (т.е. допускает все неопределенные типы) и не допускает неограниченного расширения. Это набор наборов, которые не являются членами самих себя. Эта неспособность быть непротиворечивой и полностью перечисляемой по всем возможным расширениям является теоремой Гёделя о неполноте.
  • Принцип подстановки Лискова : как правило, это неразрешимая проблема, является ли какой-либо набор подмножеством другого, то есть наследование обычно неразрешимо.
  • Ссылка Линского : неразрешимо, что такое вычисление чего-либо, когда оно описывается или воспринимается, то есть восприятие (реальность) не имеет абсолютной точки отсчета.
  • Теорема Коуза : нет никакой внешней опорной точки, таким образом , любой барьер для неограниченных возможностей внешних потерпит неудачу.
  • Второй закон термодинамики : вся вселенная (закрытая система, т.е. все) стремится к максимальному беспорядку, то есть максимально независимым возможностям.
40
23.05.2017 12:26:33
@Shelyby: Вы перепутали слишком много вещей. Вещи не так запутаны, как вы их заявляете. Многие из ваших теоретических утверждений основаны на неубедительных основаниях, таких как: «Для того, чтобы знание существовало, неожиданных возможностей много, .........« И », как правило, неразрешима проблема, является ли какой-либо набор подмножеством другого, т.е. наследование вообще неразрешимо ». Вы можете запустить отдельный блог для каждого из этих пунктов. В любом случае, ваши утверждения и предположения весьма сомнительны. Нельзя использовать вещи, о которых никто не знает!
aknon 27.12.2013 05:03:34
@aknon У меня есть блог, который объясняет эти вопросы более подробно. Моя модель TOE бесконечного пространства-времени - это неограниченные частоты. Меня не смущает, что у рекурсивной индуктивной функции есть известное начальное значение с бесконечной конечной границей, или у коиндуктивной функции есть неизвестное конечное значение и известная начальная граница. Относительность - проблема, как только вводится рекурсия. Вот почему полный Тьюринг эквивалентен неограниченной рекурсии .
Shelby Moore III 16.03.2014 08:38:07
@ShelbyMooreIII Вы идете в слишком многих направлениях. Это не ответ.
Soldalma 9.12.2016 14:48:19
@Soldalma это ответ. Разве вы не видите это в разделе Ответ. Ваш комментарий, потому что он находится в разделе комментариев.
Shelby Moore III 23.12.2016 12:54:36
Как ваше смешивание с миром Скала!
Ehsan M. Kermani 28.06.2017 18:38:39

Важным примером использования LSP является тестирование программного обеспечения .

Если у меня есть класс A, который является LSP-совместимым подклассом B, то я могу повторно использовать набор тестов B для тестирования A.

Чтобы полностью протестировать подкласс A, мне, вероятно, нужно добавить еще несколько тестовых случаев, но как минимум я могу повторно использовать все тестовые случаи суперкласса B.

Это можно понять, построив то, что Макгрегор называет «параллельной иерархией для тестирования»: мой ATestкласс унаследован от BTest. Затем требуется некоторая форма внедрения, чтобы тест-кейс работал с объектами типа A, а не типа B (подойдет простой шаблонный метод).

Обратите внимание, что повторное использование набора супер-тестов для всех реализаций подкласса фактически является способом проверки того, что эти реализации подкласса совместимы с LSP. Таким образом, можно также утверждать, что следует запускать тестовый набор суперкласса в контексте любого подкласса.

См. Также ответ на вопрос Stackoverflow: « Могу ли я реализовать серию повторно используемых тестов для проверки реализации интерфейса? »

18
23.05.2017 11:55:00

Я рекомендую вам прочитать статью: Нарушение принципа подстановки Лискова (LSP) .

Там вы можете найти объяснение принципа подстановки Лискова, общие подсказки, помогающие угадать, если вы уже нарушили его, и пример подхода, который поможет вам сделать вашу иерархию классов более безопасной.

2
9.09.2013 07:32:23

Некоторое дополнение:
я удивляюсь, почему никто не написал об Инварианте, предварительных условиях и пост-условиях базового класса, которым должны следовать производные классы. Чтобы производный класс D полностью соответствовал базовому классу B, класс D должен соответствовать определенным условиям:

  • Варианты базового класса должны быть сохранены производным классом
  • Предварительные условия базового класса не должны быть усилены производным классом
  • Постусловия базового класса не должны быть ослаблены производным классом.

Таким образом, производный должен знать о трех вышеупомянутых условиях, наложенных базовым классом. Следовательно, правила подтипирования заранее определены. Это означает, что отношения «IS A» должны соблюдаться только тогда, когда подтип подчиняется определенным правилам. Эти правила в форме инвариантов, предварительных условий и постусловий должны определяться формальным « контрактом на проектирование ».

Дальнейшие обсуждения этого доступны в моем блоге: принцип замещения Лискова

7
27.12.2013 05:20:22

Квадрат - это прямоугольник, ширина которого равна высоте. Если квадрат устанавливает два разных размера для ширины и высоты, он нарушает инвариант квадрата. Это обходится путем введения побочных эффектов. Но если у прямоугольника есть setSize (высота, ширина) с предварительным условием 0 <высота и 0 <ширина. Метод производного подтипа требует height == width; более сильное предварительное условие (и это нарушает LSP). Это показывает, что хотя квадрат является прямоугольником, он не является допустимым подтипом, поскольку предварительное условие усиливается. Обход (вообще плохая вещь) вызывает побочный эффект, и это ослабляет почтовое условие (которое нарушает lsp). У setWidth на базе есть условие post 0 <width. Производная ослабляет его с высотой == ширина.

Поэтому квадрат с изменяемым размером не является прямоугольником с изменяемым размером.

4
27.07.2015 21:56:40

Самым ясным объяснением для LSP, которое я нашел до сих пор, было «Принцип подстановки Лискова говорит, что объект производного класса должен иметь возможность заменить объект базового класса без внесения каких-либо ошибок в систему или изменения поведения базового класса». "от сюда . В статье приведен пример кода для нарушения LSP и его исправления.

3
3.05.2016 19:34:38
Пожалуйста, предоставьте примеры кода на stackoverflow.
sebenalern 3.05.2016 19:51:01

ПРИНЦИП ЗАМЕНЫ ЛИСКОВ (Из книги Марка Симанна) утверждает, что мы должны иметь возможность заменить одну реализацию интерфейса на другую, не нарушая ни клиента, ни реализацию. Это тот принцип, который позволяет удовлетворять требования, возникающие в будущем, даже если мы можем ' не предвидеть их сегодня.

Если мы отсоединяем компьютер от стены (Внедрение), ни настенная розетка (Интерфейс), ни компьютер (Клиент) не выходят из строя (фактически, если это портативный компьютер, он может даже работать от батарей в течение некоторого времени) , Однако с программным обеспечением клиент часто ожидает, что услуга будет доступна. Если служба была удалена, мы получаем исключение NullReferenceException. Чтобы справиться с ситуацией такого типа, мы можем создать реализацию интерфейса, который «ничего не делает». Это шаблон проектирования, известный как Null Object, [4], и он примерно соответствует отключению компьютера от стены. Поскольку мы используем слабую связь, мы можем заменить реальную реализацию чем-то, что ничего не делает, не вызывая проблем.

2
12.08.2016 17:16:29

Есть контрольный список, чтобы определить, нарушаете ли вы Лискова.

  • Если вы нарушаете один из следующих пунктов -> вы нарушаете Лисков.
  • Если вы не нарушаете ничего -> не могу ничего заключить.

Контрольный список:

  • В производном классе не должно быть новых исключений : если ваш базовый класс генерировал ArgumentNullException, тогда вашим подклассам было разрешено только генерировать исключения типа ArgumentNullException или любые исключения, полученные из ArgumentNullException. Бросок IndexOutOfRangeException является нарушением Лискова.
  • Предварительные условия не могут быть усилены : предположим, что ваш базовый класс работает с членом int. Теперь ваш подтип требует, чтобы int был положительным. Это улучшило предварительные условия, и теперь любой код, который до этого работал отлично с отрицательными значениями, не работает.
  • Постусловия не могут быть ослаблены : предположим, что ваш базовый класс требует, чтобы все соединения с базой данных были закрыты до возврата метода. В вашем подклассе вы отвергли этот метод и оставили открытое соединение для дальнейшего повторного использования. Вы ослабили пост-условия этого метода.
  • Инварианты должны быть сохранены : самое трудное и болезненное ограничение для выполнения. Инварианты некоторое время скрыты в базовом классе, и единственный способ выявить их - прочитать код базового класса. По сути, вы должны быть уверены, что при переопределении метода все неизменное должно оставаться неизменным после выполнения переопределенного метода. Лучшее, что я могу придумать, это применить эти инвариантные ограничения в базовом классе, но это будет нелегко.
  • Ограничение истории : при переопределении метода нельзя изменять неизменяемое свойство в базовом классе. Взгляните на этот код, и вы увидите, что Имя определено как немодифицируемое (закрытый набор), но SubType представляет новый метод, который позволяет модифицировать его (посредством отражения):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Есть еще 2 пункта: Контравариантность аргументов метода и Ковариантность возвращаемых типов . Но это не возможно в C # (я разработчик C #), поэтому мне плевать на них.

Справка:

21
12.04.2017 07:31:17
Я также являюсь разработчиком на C # и скажу, что ваше последнее утверждение не относится к Visual Studio 2010 с платформой .Net 4.0. Ковариантность возвращаемых типов допускает более производный возвращаемый тип, чем тот, который был определен интерфейсом. Пример: Пример: IEnumerable <T> (T является ковариантным) IEnumerator <T> (T является ковариантным) IQueryable <T> (T является ковариантным) IGrouping <TKey, TElement> (TKey и TElement являются ковариантными) IComparer <T> (T противоречиво) IEqualityComparer <T> (T противоречиво) IComparable <T> (T противоречиво
LCarter 5.09.2017 03:48:34
Отличный и сфокусированный ответ (хотя оригинальные вопросы касались примеров больше, чем правил).
Mike 12.06.2018 13:01:16

Полагаю, что все понимали, что такое LSP технически: вы в основном хотите абстрагироваться от деталей подтипов и безопасно использовать супертипы.

Итак, у Лискова есть 3 базовых правила:

  1. Правило подписи: должна быть правильная реализация каждой операции надтипа в подтипе синтаксически. Что-то, что компилятор сможет проверить для вас. Существует небольшое правило о том, что нужно генерировать меньше исключений и быть по крайней мере таким же доступным, как и методы супертипа.

  2. Правило методов: реализация этих операций семантически обоснована.

    • Более слабые предварительные условия: функции подтипа должны принимать по крайней мере то, что супертип принял в качестве входных данных, если не больше.
    • Более сильные постусловия: они должны создавать подмножество вывода, созданного методами супертипа.
  3. Правило свойств: это выходит за рамки отдельных вызовов функций.

    • Инварианты: вещи, которые всегда верны, должны оставаться верными. Например. Размер набора никогда не бывает отрицательным.
    • Эволюционные свойства: обычно что-то связанное с неизменяемостью или типом состояний, в которых может находиться объект. Или, возможно, объект только растет и никогда не сжимается, поэтому методы подтипа не должны этого делать.

Все эти свойства должны быть сохранены, и дополнительная функциональность подтипа не должна нарушать свойства супертипа.

Если об этих трех вещах заботятся, вы абстрагируетесь от базовых вещей и пишете слабосвязанный код.

Источник: Разработка программ на Java - Барбара Лисков

19
18.12.2016 16:54:19

Подстановочность является принципом объектно-ориентированного программирования, утверждающим, что в компьютерной программе, если S является подтипом T, объекты типа T могут быть заменены объектами типа S

давайте сделаем простой пример на Java:

Плохой пример

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Утка может летать, потому что это птица, но как насчет этого?

public class Ostrich extends Bird{}

Страус - это птица, но он не может летать, класс Страус - это подтип класса Bird, но он не может использовать метод fly, это означает, что мы нарушаем принцип LSP.

Хороший пример

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 
161
21.11.2018 15:28:37
Хороший пример, но что бы вы сделали, если бы у клиента Bird bird. Вы должны бросить объект в FlyingBirds, чтобы использовать муху, что не очень хорошо, верно?
Moody 20.11.2017 01:05:06
Нет. Если клиент имеет Bird bird, это означает, что он не может использовать fly(). Вот и все. Передача Duckне меняет этот факт. Если клиент имеет FlyingBirds bird, то, даже если он прошел, Duckон всегда должен работать одинаково.
Steve Chamaillard 18.02.2018 18:56:35
Разве это не послужило бы хорошим примером для разделения интерфейса?
Saharsh 5.03.2019 18:50:05
Отличный пример, спасибо Человек
Abdelhadi Abdo 28.05.2019 11:19:02
Как насчет использования интерфейса «Flyable» (не могу придумать лучшего названия). Таким образом, мы не вверяем себя в эту жесткую иерархию. Если только мы не знаем, что это действительно нужно.
Thirdy 28.05.2019 12:32:41

В очень простом предложении мы можем сказать:

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

8
16.08.2017 09:09:53

Принцип подстановки Ликова гласит, что если программный модуль использует базовый класс, то ссылку на базовый класс можно заменить производным классом, не влияя на функциональность программного модуля.

Намерение - Производные типы должны полностью заменять свои базовые типы.

Пример - ко-вариантные типы возврата в Java.

2
23.09.2017 19:26:00

Допустим, мы используем прямоугольник в нашем коде

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

В нашем классе геометрии мы узнали, что квадрат - это особый тип прямоугольника, потому что его ширина равна длине его высоты. Давайте также создадим Squareкласс на основе этой информации:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Если мы заменим Rectangleс Squareв нашем первом коде, то он сломается:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Это происходит потому , что Squareесть новое предварительное условие у нас не было в Rectangleклассе: width == height. Согласно LSP Rectangleэкземпляры должны заменяться Rectangleэкземплярами подкласса. Это связано с тем, что эти экземпляры проходят проверку типов для Rectangleэкземпляров, и поэтому они вызывают непредвиденные ошибки в вашем коде.

Это был пример для части «предварительные условия не могут быть усилены в подтипе» в статье вики . Итак, подведем итог: нарушение LSP в какой-то момент может вызвать ошибки в вашем коде.

3
9.10.2017 04:26:28

Длинная короткая история, давайте оставить прямоугольники прямоугольники и квадраты квадратов, практический пример , когда расширение родительского класса, вы должны либо сохранить точный родительский API или удлинить его.

Допустим, у вас есть база ItemsRepository.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

И подкласс, расширяющий его:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Тогда у вас мог бы быть Клиент, работающий с API Base ItemsRepository и опирающийся на него.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

LSP нарушается , когда подставляя родительский класс с суб брейков класса контракта АНИ в .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Вы можете узнать больше о написании поддерживаемого программного обеспечения в моем курсе: https://www.udemy.com/enterprise-php/

19
18.12.2018 03:27:12

Я вижу прямоугольники и квадраты в каждом ответе, и как нарушать LSP.

Я хотел бы показать, как LSP может быть согласован с реальным примером:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Этот дизайн соответствует LSP, потому что поведение остается неизменным независимо от реализации, которую мы решили использовать.

И да, вы можете нарушить LSP в этой конфигурации, сделав одно простое изменение следующим образом:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Теперь подтипы нельзя использовать одинаково, поскольку они больше не дают того же результата.

20
4.05.2019 08:15:32
Пример не нарушает LSP только до тех пор, пока мы ограничиваем семантику Database::selectQueryподдержки только подмножества SQL, поддерживаемого всеми механизмами БД. Это вряд ли практично ... Тем не менее, пример все еще легче понять, чем большинство других, используемых здесь.
Palec 25.02.2018 13:02:44
Я нашел этот ответ легче всего понять из остальных.
Malcolm Salvador 11.02.2019 17:14:00

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

Все время мы разрабатываем программный модуль и создаем некоторые иерархии классов. Затем мы расширяем некоторые классы, создавая некоторые производные классы.

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

Принцип подстановки Лискова гласит, что если программный модуль использует базовый класс, то ссылка на базовый класс может быть заменена производным классом, не влияя на функциональность программного модуля.

Пример:

Ниже приведен классический пример нарушения принципа подстановки Лискова. В примере используются 2 класса: Rectangle и Square. Давайте предположим, что объект Rectangle используется где-то в приложении. Расширяем приложение и добавляем класс Square. Класс square возвращается фабричным шаблоном, основанным на некоторых условиях, и мы не знаем точно, какой тип объекта будет возвращен. Но мы знаем, что это прямоугольник. Мы получаем объект прямоугольника, устанавливаем ширину 5 и высоту 10 и получаем площадь. Для прямоугольника с шириной 5 и высотой 10 площадь должна быть 50. Вместо этого результат будет 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Заключение:

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

Смотрите также: принцип Open Close

Некоторые похожие концепции для лучшей структуры: Соглашение по конфигурации

9
21.05.2018 09:01:48

Вот выдержка из этого поста, в которой все проясняется:

[..] Чтобы понять некоторые принципы, важно понимать, когда они были нарушены. Это то, что я буду делать сейчас.

Что означает нарушение этого принципа? Это подразумевает, что объект не выполняет контракт, наложенный абстракцией, выраженной интерфейсом. Другими словами, это означает, что вы неправильно определили свои абстракции.

Рассмотрим следующий пример:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Это нарушение LSP? Да. Это связано с тем, что договор об аккаунте говорит нам, что аккаунт будет отозван, но это не всегда так. Итак, что я должен сделать, чтобы это исправить? Я просто изменяю договор:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Вуаля, сейчас контракт выполнен.

Это тонкое нарушение часто навязывает клиенту способность различать конкретные используемые объекты. Например, учитывая контракт первого Аккаунта, он может выглядеть следующим образом:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

И это автоматически нарушает принцип открытого-закрытого доступа [то есть требования снятия денег. Потому что вы никогда не знаете, что происходит, если у объекта, нарушающего договор, не хватает денег. Возможно, он просто ничего не возвращает, возможно, будет выдано исключение. Таким образом, вы должны проверить, если это hasEnoughMoney()- что не является частью интерфейса. Так что эта принудительная проверка, зависящая от конкретного класса, является нарушением OCP].

Этот пункт также обращается к заблуждению, с которым я часто сталкиваюсь по поводу нарушения LSP. В нем говорится «если поведение ребенка изменилось у ребенка, то это нарушает LSP». Тем не менее, это не так - до тех пор, пока ребенок не нарушает родительский договор.

0
12.08.2018 17:10:59

Давайте проиллюстрируем на Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Здесь нет проблем, верно? Автомобиль определенно является транспортным устройством, и здесь мы видим, что он переопределяет метод startEngine () своего суперкласса.

Давайте добавим еще одно транспортное устройство:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Сейчас все идет не так, как планировалось! Да, велосипед является транспортным устройством, однако он не имеет двигателя и, следовательно, метод startEngine () не может быть реализован.

Это те проблемы, к которым приводит нарушение принципа подстановки Лискова, и чаще всего их можно распознать с помощью метода, который ничего не делает или даже не может быть реализован.

Решением этих проблем является правильная иерархия наследования, и в нашем случае мы решили бы эту проблему путем дифференциации классов транспортных устройств с двигателями и без них. Хотя велосипед - это транспортное средство, у него нет двигателя. В этом примере наше определение транспортного устройства неверно. У него не должно быть двигателя.

Мы можем реорганизовать наш класс TransportationDevice следующим образом:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Теперь мы можем расширить TransportationDevice для немоторизованных устройств.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

И расширить транспортное устройство для моторизованных устройств. Здесь более уместно добавить объект Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

Таким образом, наш класс автомобилей становится более специализированным, придерживаясь принципа подстановки Лискова.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

И наш велосипедный класс также соответствует принципу замещения Лискова.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
13
10.02.2019 10:56:06

В LSP говорится, что «объекты должны быть заменены их подтипами». С другой стороны, этот принцип указывает на

Дочерние классы никогда не должны нарушать определения типов родительского класса.

и следующий пример помогает лучше понять LSP.

Без LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Фиксация по LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}
3
7.04.2019 10:46:14

Позвольте мне попробовать, рассмотрим интерфейс:

interface Planet{
}

Это реализуется классом:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Вы будете использовать Землю как:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Теперь рассмотрим еще один класс, расширяющий Землю:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

Теперь, согласно LSP, вы должны иметь возможность использовать LiveablePlanet вместо Земли, и это не должно сломать вашу систему. Подобно:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Примеры взяты отсюда

0
26.04.2019 06:59:43
Бедный пример. Earthявляется примером того plant, почему он будет получен из этого?
zar 3.05.2019 15:46:33

Принцип подстановки Лискова

  • Переопределенный метод не должен оставаться пустым
  • Переопределенный метод не должен выдавать ошибку
  • Поведение базового класса или интерфейса не должно изменяться (переделываться) из-за поведения производного класса.
5
19.08.2019 11:55:07

Проще говоря, LSP гласит, что объекты одного и того же суперкласса должны иметь возможность обмениваться друг с другом, ничего не нарушая.

Например, если у нас есть Catи в Dogкласс , производный от Animalкласса, любые функции , использующие класс Animal должны быть в состоянии использовать Catили Dogи ведут себя нормально.

4
24.01.2020 12:19:38