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

908

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

NotMyself
источник
Больше примеров присоединения и нарушения LSP здесь
StuartLC
1
Этот вопрос имеет бесконечно много хороших ответов и поэтому слишком широк .
Raedwald

Ответы:

892

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

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

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

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

Вы все должны проверить другие бесценные мотивационные плакаты Принципов SOLID .

м-диез
источник
19
@ m-sharp Что, если это неизменный прямоугольник, такой, что вместо SetWidth и SetHeight у нас вместо этого есть методы GetWidth и GetHeight?
Pacerier
140
Мораль истории: моделируйте свои классы, основываясь на поведении, а не на свойствах; смоделируйте свои данные на основе свойств, а не на поведении. Если он ведет себя как утка, это, безусловно, птица.
Скливвз
193
Ну, квадрат явно является типом прямоугольника в реальном мире. Можем ли мы смоделировать это в нашем коде, зависит от спецификации. LSP указывает, что поведение подтипа должно соответствовать поведению базового типа, как определено в спецификации базового типа. Если спецификация базового типа прямоугольника говорит, что высота и ширина могут быть установлены независимо, то LSP говорит, что квадрат не может быть подтипом прямоугольника. Если спецификация прямоугольника говорит, что прямоугольник является неизменным, то квадрат может быть подтипом прямоугольника. Все дело в подтипах, поддерживающих поведение, указанное для базового типа.
SteveT
63
@Pacerier нет проблем, если он неизменен. Реальная проблема здесь заключается в том, что мы моделируем не прямоугольники, а скорее «изменяемые прямоугольники», то есть прямоугольники, ширину или высоту которых можно изменить после создания (и мы по-прежнему считаем, что это один и тот же объект). Если мы посмотрим на класс прямоугольника таким образом, то станет ясно, что квадрат не является «изменяемым прямоугольником», потому что квадрат не может быть изменен и все же будет квадратом (в общем). Математически мы не видим проблемы, потому что изменчивость даже не имеет смысла в математическом контексте.
asmeurer
14
У меня есть один вопрос о принципе. Почему бы проблема , если Square.setWidth(int width)была реализована следующим образом: this.width = width; this.height = width;? В этом случае гарантируется, что ширина равна высоте.
MC Emperor
488

Принцип замещения Лискова (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.

NotMyself
источник
10
См. Также Задача о круговом эллипсе в Википедии, чтобы найти аналогичный, но более простой пример.
Брайан
Requote от @NotMySelf: «Я думаю, что пример просто демонстрирует, что наследование от платы не имеет смысла в контексте ThreeDBoard, и все сигнатуры методов не имеют смысла с осью Z».
Контанго
1
Так что, если мы добавим другой метод в класс Child, но все функции Parent все еще имеют смысл в классе Child, это нарушит LSP? Поскольку, с одной стороны, мы немного изменили интерфейс для использования дочернего элемента, с другой стороны, если мы приведем дочерний элемент к родительскому типу, код, который ожидает, что родительский элемент будет работать нормально.
Николай Кондратьев
5
Это антилисковский пример. Лисков заставляет нас выводить прямоугольник из квадрата. Класс больше параметров из класса меньше параметров. И вы хорошо показали, что это плохо. Это действительно хорошая шутка, чтобы пометить в качестве ответа и получить 200 голосов за антилисовский ответ на вопрос о Лискове. Действительно ли принцип Лискова ошибочен?
Гангнус
3
Я видел, как наследование работает неправильно. Вот пример. Базовый класс должен быть 3DBoard и производным классом Board. У доски все еще есть ось Z: Макс (Z) = Мин (Z) = 1
Паулюс
169

Подстановочность является принципом объектно-ориентированного программирования, утверждающим, что в компьютерной программе, если 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{} 
Майсара Альхинди
источник
3
Хороший пример, но что бы вы сделали, если бы у клиента Bird bird. Вы должны бросить объект в FlyingBirds, чтобы использовать муху, что не очень хорошо, верно?
Moody
17
Нет. Если клиент имеет Bird bird, это означает, что он не может использовать fly(). Вот и все. Передача Duckне меняет этот факт. Если клиент имеет FlyingBirds bird, то, даже если он прошел, Duckон всегда должен работать одинаково.
Стив Шамайяр
9
Разве это не послужило бы хорошим примером для разделения интерфейса?
Saharsh
Отличный пример, спасибо Человек
Абдельхади Абдо
6
Как насчет использования интерфейса «Flyable» (не могу придумать лучшего названия). Таким образом, мы не вверяем себя в эту жесткую иерархию. Если только мы не знаем, что это действительно нужно.
Третье
132

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.

Конрад Рудольф
источник
35
И, следовательно, сложность использования «ОО» для моделирования всего, что мы могли бы захотеть на самом деле моделировать.
DrPizza
9
@DrPizza: Абсолютно. Однако две вещи. Во-первых, такие отношения все еще можно смоделировать в ООП, хотя и не полностью или с использованием более сложных обходных путей (выберите тот, который подходит вашей проблеме). Во-вторых, нет лучшей альтернативы. Другие сопоставления / моделирования имеют те же или похожие проблемы. ;-)
Конрад Рудольф
7
@NickW В некоторых случаях (но не в приведенном выше) вы можете просто инвертировать цепочку наследования - логически говоря, 2D-точка - это 3D-точка, где не учитывается третье измерение (или 0 - все точки лежат в одной плоскости в 3D пространство). Но это, конечно, не очень практично. В общем, это один из случаев, когда наследование на самом деле не помогает, и между сущностями нет естественных отношений. Смоделируйте их отдельно (по крайней мере, я не знаю лучшего способа).
Конрад Рудольф
7
ООП предназначен для моделирования поведения, а не данных. Ваши занятия нарушают инкапсуляцию еще до нарушения LSP.
Скливвз
2
@AustinWBryan Yep; Чем дольше я работаю в этой области, тем больше я склонен использовать наследование только для интерфейсов и абстрактных базовых классов, а для остальных - композицию. Иногда это немного больше работы (печатать мудро), но это позволяет избежать целого ряда проблем и широко повторяется советами других опытных программистов.
Конрад Рудольф
77

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

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

Простой пример нарушения 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объект будет поврежден, потому что высота не будет изменена. Это явное нарушение ЛСП. Функция не работает для производных своих аргументов.

[...]

Филипп Уэллс
источник
14
Поздно, но я подумал, что в этой статье была интересная цитата: 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.15
@ user2023861 Вы совершенно правы. Я напишу ответ на основании этого.
inf3rno
40

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% -ное покрытие всех возможных инвариантов на языке полного языка Тьюринга. Чтобы знания существовали, неожиданных возможностей много, то есть беспорядок и энтропия всегда должны увеличиваться. Это энтропийная сила. Чтобы доказать все возможные вычисления потенциального расширения, нужно заранее вычислить все возможные расширения.

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

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

  • Теоремы Гёделя о неполноте : любая формальная теория, в которой все арифметические истины могут быть доказаны, противоречива.
  • Парадокс Рассела : каждое правило членства для набора, которое может содержать набор, перечисляет конкретный тип каждого члена или содержит себя. Таким образом, множества либо не могут быть расширены, либо являются неограниченной рекурсией. Например, набор всего, что не является чайником, включает в себя, включает себя, включает себя и т. Д. Таким образом, правило является непоследовательным, если оно (может содержать набор и) не перечисляет конкретные типы (т.е. допускает все неопределенные типы) и не допускает неограниченного расширения. Это набор наборов, которые не являются членами самих себя. Эта неспособность быть непротиворечивой и полностью перечисляемой по всем возможным расширениям является теоремой Гёделя о неполноте.
  • Принцип подстановки Лискова : как правило, это неразрешимая проблема, является ли какой-либо набор подмножеством другого, то есть наследование обычно неразрешимо.
  • Ссылка на Линского : неразрешимо, что такое вычисление чего-либо, когда оно описывается или воспринимается, то есть восприятие (реальность) не имеет абсолютной точки отсчета.
  • Теорема Коуза : нет никакой внешней опорной точки, таким образом , любой барьер для неограниченных возможностей внешних потерпит неудачу.
  • Второй закон термодинамики : вся вселенная (закрытая система, т.е. все) стремится к максимальному беспорядку, то есть максимально независимым возможностям.
Шелби Мур III
источник
17
@Shelyby: Вы перепутали слишком много вещей. Вещи не так запутаны, как вы их заявляете. Многие из ваших теоретических утверждений основаны на неубедительных основаниях, таких как: «Для того, чтобы знания существовали, неожиданных возможностей много, .........« И », как правило, это неразрешимая проблема, является ли какой-либо набор подмножеством другого, т.е. наследование вообще неразрешимо ». Вы можете запустить отдельный блог для каждого из этих пунктов. В любом случае, ваши утверждения и предположения весьма сомнительны. Нельзя использовать вещи, о которых никто не знает!
Aknon
1
@aknon У меня есть блог, который объясняет эти вопросы более подробно. Моя модель TOE бесконечного пространства-времени - это неограниченные частоты. Меня не смущает, что у рекурсивной индуктивной функции есть известное начальное значение с бесконечной конечной границей, или у коиндуктивной функции есть неизвестное конечное значение и известная начальная граница. Относительность - проблема, как только вводится рекурсия. Вот почему полная Тьюринга эквивалентна неограниченной рекурсии .
Шелби Мур III
4
@ShelbyMooreIII Вы идете в слишком многих направлениях. Это не ответ.
Солдальма
1
@Soldalma это ответ. Разве вы не видите это в разделе Ответ. Ваш комментарий, потому что он находится в разделе комментариев.
Шелби Мур III
1
Как ваше смешивание с миром Скала!
Эсан М. Кермани
24

Я вижу прямоугольники и квадраты в каждом ответе, и как нарушать 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 !
    }
}

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

Стив Шамайяр
источник
6
Пример не нарушает LSP только до тех пор, пока мы ограничиваем семантику Database::selectQueryподдержки только подмножества SQL, поддерживаемого всеми механизмами БД. Это вряд ли практично ... Тем не менее, пример все еще легче понять, чем большинство других, используемых здесь.
Palec
5
Я нашел этот ответ легче всего понять из остальных.
Малкольм Сальвадор
23

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

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

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

  • В производном классе не должно быть новых исключений : если ваш базовый класс генерировал 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 #), поэтому мне плевать на них.

Ссылка:

Cù Đức Hiếu
источник
Я также являюсь разработчиком на 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
1
Отличный и сфокусированный ответ (хотя оригинальные вопросы касались не просто правил, а примеров).
Майк
22

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 одинаков.

Чарли Мартин
источник
9
Но ... если вы всегда получаете одно и то же поведение, то какой смысл иметь производный класс?
Леонид
2
Вы упустили момент: это то же самое наблюдаемое поведение. Вы можете, например, заменить что-то с производительностью O (n) на что-то функционально эквивалентное, но с производительностью O (lg n). Или вы можете заменить что-то, что обращается к данным, реализованным с MySQL, и заменить это базой данных в памяти.
Чарли Мартин
@Charlie Martin, кодирование интерфейса, а не реализация - я копаю это. Это не уникально для ООП; функциональные языки, такие как Clojure, также способствуют этому. Даже с точки зрения Java или C #, я думаю, что использование интерфейса вместо абстрактного класса плюс иерархии классов будет естественным для примеров, которые вы предоставляете. Python не является строго типизированным и на самом деле не имеет интерфейсов, по крайней мере, явно. Моя трудность заключается в том, что я занимаюсь ООП в течение нескольких лет, не придерживаясь SOLID. Теперь, когда я столкнулся с этим, это кажется ограничивающим и почти противоречивым.
Хэмиш Грубиджан
Ну, тебе нужно вернуться и проверить оригинальную статью Барбары. reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps Это на самом деле не указано в терминах интерфейсов, и это логическое отношение, которое сохраняется (или не имеет) в любом язык программирования, который имеет некоторую форму наследования.
Чарли Мартин
1
@HamishGrubijan Я не знаю, кто сказал вам, что Python не печатается строго, но они лгали вам (и если вы мне не верите, запустите интерпретатор Python и попробуйте 2 + "2"). Возможно, вы путаете «строго типизированный» со «статически типизированным»?
asmeurer
21

Длинная короткая история, давайте оставить прямоугольники прямоугольники и квадраты квадратов, практический пример , когда расширение родительского класса, вы должны либо сохранить точный родительский 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/

Лукас Лукач
источник
20

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

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

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

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

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

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

Крис Аммерман
источник
19

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

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

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

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

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

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

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

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

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

snagpaul
источник
18

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

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

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

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

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

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

avandeursen
источник
14

Давайте проиллюстрируем на 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() { ... }
}
Халед Касем
источник
9

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

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

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

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

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

Дэмиен Поллет
источник
2
Хм, эта формулировка принадлежит Барбаре Лисков. Барбара Лисков, «Абстракция данных и иерархия», SIGPLAN Notices, 23,5 (май 1988 г.). Оно не «слишком сильное», оно «совершенно правильное» и не имеет значения, которое, по вашему мнению, имеет. Он сильный, но обладает достаточным количеством силы.
DrPizza
Тогда в реальной жизни очень мало подтипов :)
Дэмиен Поллет
3
«Поведение не изменяется» не означает, что подтип даст вам точно такое же конкретное значение (и) результата. Это означает, что поведение подтипа соответствует ожидаемому в базовом типе. Пример: базовый тип Shape может иметь метод draw () и предусматривать, что этот метод должен отображать форму. Два подтипа Shape (например, Square и Circle) будут реализовывать метод draw (), и результаты будут выглядеть по-разному. Но пока поведение (рендеринг фигуры) соответствует указанному поведению Shape, тогда Square и Circle будут подтипами Shape в соответствии с LSP.
SteveT
9

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

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

Алиреза Рахмани Халили
источник
9

Принцип замещения Лискова (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

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

Гауранг Омар
источник
8

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

  • Переопределенный метод не должен оставаться пустым
  • Переопределенный метод не должен выдавать ошибку
  • Поведение базового класса или интерфейса не должно изменяться (переделываться), как из-за поведения производного класса.
Rahamath
источник
7

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

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

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

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

aknon
источник
6

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

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

johannesMatevosyan
источник
4

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

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

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

Том Хотин - Tackline
источник
1
Я думаю, что этот пример просто демонстрирует, что наследование от платы не имеет смысла в контексте ThreeDBoard, и все сигнатуры методов не имеют смысла с осью Z.
NotMyself
4

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

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

Wouter
источник
4

Этот принцип был введен Барбарой Лисков в 1987 году и расширяет принцип Open-Closed, сосредоточив внимание на поведении суперкласса и его подтипов.

Его важность становится очевидной, когда мы рассматриваем последствия его нарушения. Рассмотрим приложение, которое использует следующий класс.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

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

public class Square : Rectangle
{
} 

Однако при этом мы столкнемся с двумя проблемами:

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

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Теперь, когда кто-то установит ширину квадратного объекта, его высота изменится соответственно и наоборот.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Давайте двигаться вперед и рассмотрим эту другую функцию:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

Если мы передадим ссылку на квадратный объект в эту функцию, мы нарушим LSP, потому что функция не работает для производных своих аргументов. Свойства width и height не являются полиморфными, потому что они не объявлены виртуальными в прямоугольнике (квадратный объект будет поврежден, потому что высота не будет изменена).

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

Иван Порта
источник
3

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

Prasa
источник
1
Пожалуйста, предоставьте примеры кода на stackoverflow.
Себеналерн
3

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

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 в какой-то момент может вызвать ошибки в вашем коде.

inf3rno
источник
3

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();
}
Zahra.HY
источник
2

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

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

Рышард Деган
источник
2

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

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

Рагу Редди Муттана
источник
2

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

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

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

Ишан Аггарвал
источник
1

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

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

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

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

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». Тем не менее, это не так - до тех пор, пока ребенок не нарушает родительский договор.

Вадим Самохин
источник