Каков пример принципа замещения Лискова?

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


person NotMyself    schedule 11.09.2008    source источник
comment
Дополнительные примеры соблюдения и нарушения LSP здесь   -  person StuartLC    schedule 15.05.2015
comment
На этот вопрос бесконечно много хороших ответов, поэтому он слишком широк.   -  person Raedwald    schedule 16.12.2018
comment
12 лет спустя было бы здорово, если бы вы могли отклонить свой ответ и принять другой, поскольку ваш собственный ответ, к сожалению, совершенно неверен и описывает не LSP, а несоответствие интерфейса. И, поскольку это принято, это вводит в заблуждение множество людей (о чем свидетельствует количество положительных голосов).   -  person Konrad Rudolph    schedule 13.10.2020
comment
Это подходит для меня. Перешли на самый популярный ответ.   -  person NotMyself    schedule 13.10.2020
comment
число тысяч голосов!   -  person Jonas Grønbek    schedule 06.06.2021


Ответы (33)


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

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

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

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

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

person m-sharp    schedule 25.02.2009
comment
@ m-sharp Что делать, если это неизменный прямоугольник, такой, что вместо SetWidth и SetHeight у нас есть методы GetWidth и GetHeight? - person Pacerier; 26.04.2012
comment
Мораль истории: моделируйте свои классы на основе поведения, а не свойств; моделируйте свои данные на основе свойств, а не поведения. Если он ведет себя как утка, это, безусловно, птица. - person Sklivvz; 20.05.2012
comment
Что ж, квадрат явно является разновидностью прямоугольника в реальном мире. Сможем ли мы смоделировать это в нашем коде, зависит от спецификации. LSP указывает, что поведение подтипа должно соответствовать поведению базового типа, как определено в спецификации базового типа. Если в спецификации базового типа прямоугольника указано, что высоту и ширину можно устанавливать независимо, то LSP сообщает, что квадрат не может быть подтипом прямоугольника. Если в спецификации прямоугольника указано, что прямоугольник неизменяем, то квадрат может быть подтипом прямоугольника. Все дело в подтипах, поддерживающих поведение, указанное для базового типа. - person SteveT; 24.09.2012
comment
@Pacerier нет проблем, если он неизменяем. Настоящая проблема здесь в том, что мы моделируем не прямоугольники, а прямоугольники с возможностью изменения формы, то есть прямоугольники, ширина или высота которых может быть изменена после создания (и мы по-прежнему считаем это одним и тем же объектом). Если мы посмотрим на класс прямоугольника таким образом, станет ясно, что квадрат не является изменяемым прямоугольником, потому что квадрат не может быть изменен и по-прежнему остается квадратом (в общем). Математически мы не видим проблемы, потому что изменчивость не имеет смысла даже в математическом контексте. - person asmeurer; 20.01.2013
comment
Из лекции профессора Барбары Лисков: объекты подтипов должны вести себя как объекты супертипов, если используются через методы супертипа. - person ruhong; 04.02.2015
comment
Если ширина и высота могут быть изменены сеттерами, должен быть только класс прямоугольника, а не специальный квадратный класс. Вместо этого класс прямоугольника должен иметь метод получения с именем IsSquare. Если ширина и высота имеют одинаковые значения, IsSquare вернет true, в противном случае - false. Типы не всегда статичны, но иногда - как в этом случае - могут меняться. - person brighty; 10.02.2015
comment
так зачем мне даже подтипы, если поведение моего унаследованного класса не отличается от родительского (за исключением случаев, когда он действует так же, но по-другому)? Зачем нужны все эти методы переопределения, если они должны вести себя абсолютно так же, как базовые? Говоря о геометрических фигурах: IDrawable имеет Draw (). как вы могли настоять на том, чтобы метод Draw () Circle: IDrawable давал такой же результат, как и метод Square: IDrawable? или, скажем, Rotated90DegreesSquare: квадрат рисовать так же, как квадрат? - person jungle_mole; 11.09.2015
comment
У меня один вопрос по принципу. В чем была бы проблема, если бы Square.setWidth(int width) был реализован так: this.width = width; this.height = width;? В этом случае гарантируется, что ширина равна высоте. - person MC Emperor; 28.10.2015
comment
Этот мотивационный плакат не имеет смысла. Так и должно быть. Если он похож на утку, крякает, как утка, но нуждается в батареях ... опять же, почему вас интересуют батареи? Потому что, в конце концов, это означает, что единственная замена утке - это другая утка (лучше всего такая же). Такой радикализм обречен на провал. - person David Tonhofer; 05.01.2016
comment
Проблема квадрата-прямоугольника также известна как проблема круга-эллипса. - person mbx; 02.03.2016
comment
Правильное или неправильное, хорошее или плохое, зависит от точки зрения человека - со стороны потребителя кода или со стороны автора кода. LSP, IMO, относится к стороне потребителя. С правильной точки зрения, многие заблуждения выглядят правильными. - person Sudhir; 23.05.2016
comment
Другое решение - определить прямоугольники как сохраняющие соотношение сторон, а не как имеющие независимую ширину и высоту. - person Ed L; 16.07.2016
comment
Кажется, не ответил на вопрос. Прочитав его, я все еще не знаю, что такое LSP (если плакат не содержит определения, хотя, учитывая контекст таких плакатов, это неясно). - person iheanyi; 12.08.2016
comment
Я также не понимаю квадратного примера. Как setWidth и setHeight не имеют смысла на квадрате. Как вы очень легко объяснили, выполнение одного подразумевает другое - это всего лишь определение квадрата. Это не объясняет, почему есть что-то неправильное или несовместимое с заменой прямоугольника квадратом. - person iheanyi; 12.08.2016
comment
Не могли бы вы просто переопределить setWidth или setHeight и в определении вызвать другой метод с тем же параметром и poof, все работает и поведение согласовано? - person Ungeheuer; 22.11.2016
comment
@MCEmperor, если вы измените реализацию setHeight() и setWidth() в Square, поэтому те места в вашем коде, где вы используете Rectangule, больше не будут работать, если вы передадите Square, и это основной момент в LSP; - person sdlins; 26.12.2016
comment
Спасибо за ответ, но что с того, что LSP сломан? - person BKSpurgeon; 02.01.2017
comment
Тем не менее, изображение НЕ является допустимым примером неработающего LSP (на самом деле пахнет Reddit, и это не то, что мы хотим чувствовать здесь, на SO). Подклассы всегда будут более конкретными, чем суперклассы. Однако их особенности не обязательно нарушают LSP. Вопрос в том, влияют ли эти особенности на общий договор (и нарушают его). Нарушает ли утка с батарейным питанием общий контракт абстрактной утки, зависит от конкретных деталей конструкции. - person AnT; 28.01.2017
comment
Все используют этот квадрат против прямоугольника, что является ужасным примером. Вы переопределяете свойство ширины, чтобы установить свойство высоты, что больше похоже на побочный эффект. Все это будет решено, если вы заставите людей явно устанавливать ширину и высоту. - person johnny 5; 01.03.2017
comment
И изображение, и ссылка в конце не работают. Не могли бы вы это исправить? - person PaulB; 05.06.2017
comment
Спасибо @PaulB. Починил это. - person m-sharp; 06.06.2017
comment
На меня сейчас работают лоси-технари, @ m-sharp. Просто временное отключение, а может региональное? Ревизия 8 сейчас кажется, по крайней мере, ненужной. - person Palec; 07.06.2017
comment
Прохладный. Откатил сдачу. У SO действительно хорошая история изменений! - person m-sharp; 07.06.2017
comment
Черт, весь сайт Lot Techies теперь возвращает одну и ту же страницу с названием «Ошибка базы данных» и содержит только h1 с ошибкой при установлении соединения с базой данных. Я нашел последний снимок WayBack Machine, и в нем четко указано, что изображения лицензированы CC BY-SA. Я предлагаю вам связать снимок WayBack Machine, а также исходный URL-адрес и загрузить изображение на серверы Stack Overflow (через редактор). Таким образом, перебои в работе почты не повлияют. - person Palec; 10.06.2017
comment
o.setDimensions(width, height) решит эту проблему, но LSP все равно будет нарушен, потому что квадрат имеет более строгие предварительные условия (width == height), чем прямоугольник. Я не думаю, что ваш пост что-то отвечает о LSP. - person inf3rno; 09.10.2017
comment
Здесь есть хорошая дополнительная информация о переопределении методов / контрактах softwareengineering.stackexchange.com/a/244783/1451 - person BrunoLM; 21.10.2017
comment
Является ли прямоугольник типом квадрата или квадрат типом прямоугольника или наоборот? Прямоугольник можно определить как квадрат с неравными сторонами, а квадрат можно определить как прямоугольник с равными сторонами. Квадрат с неравными сторонами не является квадратом, но прямоугольник с равными сторонами по-прежнему остается прямоугольником и квадратом. Итак, квадрат - это прямоугольник с дополнительным контрактом. Следовательно, интерфейс для квадрата должен быть таким же, как для прямоугольника. Установка одного измерения должно установить другое, имеет смысл. - person ATL_DEV; 15.12.2017
comment
Я бы не сказал, что нет проблем, если они неизменны. Это может сбить читателя с толку, чтобы увидеть GetHeight и GetWidth, если они собираются делать то же самое. Это может быть крошечной проблемой, но все же причина сделать это по-другому. - person AustinWBryan; 09.05.2018
comment
В некоторых книгах это называется правилом% 100? - person anilkay; 17.06.2018
comment
@AustinWBryan нет, это хорошо определенные количества. Квадрат имеет высоту и ширину; они оказались равными. Если читателя это сбивает с толку, значит, он либо узнает что-то новое и важное о коде, либо ему не место :) - person Ben Kushigian; 19.06.2018
comment
Я думаю, что мы путаем подтип с частным случаем. Поведение квадрата ничем не отличается от прямоугольника. Периметр, площадь можно рассчитать по тем же формулам. Квадрат - это не улучшение или уточнение прямоугольника, это просто частный случай. Квадрат не должен быть подклассом прямоугольника, он даже не должен быть отдельным классом. - person Mircea Ion; 05.02.2019
comment
@MirceaIon, Square может быть подклассом Rectangle для упрощения интерфейса, например, с SetLength, который может внутренне вызывать SetWidth и SetHeight - person alancalvitti; 29.03.2019
comment
Можно получить Square из Rectangle, но у вас не должно быть setWidth и setHeight в этом случае. Но никто не мог помешать вам установить сеттеры для зоны, куда вы помещаете прямоугольники. В этом случае Square заполнит только часть зоны, а Rectangle заполнит ее полностью. :) - person Alex; 17.04.2019
comment
Может кто-нибудь, из любви к коду, скажите мне, как еще смоделировать этот же пример, чтобы он соответствовал LSP? - person Saurabh Goyal; 11.06.2019
comment
Однажды я провалил интервью именно по этому вопросу о квадратах. Интервьюер отклонил мой непосредственный вопрос: что мы будем делать с этими прямоугольниками и квадратами? Сегодня я счастлив, что у меня не было этой работы. :) - person Aleksei Guzev; 15.12.2019
comment
Какой дядя Боб? : -? - person Yousha Aleayoub; 05.02.2020
comment
@sdlins Я не согласен. Можете ли вы назвать одну ситуацию, когда, если поведение setHeight и setWidth перезаписано в классе Square, например, подписанный MCEmperor, код больше не будет работать для Reactangle? - person Murilo; 09.04.2020
comment
Было бы намного лучше предоставить пример использования и фрагмент кода, который продемонстрировал бы код. - person Giorgi Tsiklauri; 06.08.2020
comment
@Murilo Он должен заполнить наше поле 640x480, поэтому r.SetWidth (640); r.SetHeight (480) или наоборот не имеет значения. Любой подкласс, который предлагает возможность устанавливать ширину и высоту отдельно, но на самом деле этого не делает, что-то сломает. - person prosfilaes; 13.01.2021
comment
Проблема, которую я нахожу с методами SetWidth и SetHeight, заключается в том, что они не только нарушают LSP, но и ISP (принцип разделения интерфейсов), поэтому они должны быть разделены на два разных интерфейса, возможно, SetSize (int, int) для Rectangle и SetSize (int) для Square, потому что они разные, геттеры могут оставаться там, где они есть ... открыты для обсуждения - person Eugenio Miró; 28.01.2021
comment
Что касается утки, батареек и почему она ломает LSV - ›потому что уткам по определению батарейки не нужны. Для меня это довольно очевидно, и это метафора в форме шутки, не стоит воспринимать ее более серьезно. - person Ric Jafe; 17.03.2021
comment
@SaurabhGoyal - суть в том, что правильная реализация не должна иметь побочных эффектов одного геттера к другому свойству, поскольку их изначально не было у родителя. Например, у вас не может быть геттеров для индивидуальной высоты и ширины, а затем в виде пары и одного setDimentions (x, y), и таким образом вы можете избежать этого конкретного побочного эффекта изменения высоты при установке ширины в квадратном примере . Несомненно, есть несколько способов добиться этого. Главное, чтобы поведение ребенка отличалось от поведения родителя. Вот что нарушает LSP. - person Ric Jafe; 17.03.2021
comment
Это помогло мне понять больше, чем любой другой пример - person Tom; 24.05.2021

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

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

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

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

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

Схема классов

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

В книге также изменяются требования и говорится, что игровая структура также должна поддерживать трехмерные игровые доски, чтобы соответствовать играм, в которых есть полет. Итак, представлен 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.

person NotMyself    schedule 11.09.2008
comment
См. Также Задачу круга-эллипса в Википедии, где можно найти аналогичный, но более простой пример. - person Brian; 21.10.2011
comment
Реквотирование от @NotMySelf: я думаю, что этот пример просто демонстрирует, что наследование с доски не имеет смысла в контексте ThreeDBoard, и все сигнатуры методов бессмысленны с осью Z .. - person Contango; 05.06.2013
comment
Реквизит @Chris Ammerman: Оценка соблюдения LSP может быть отличным инструментом для определения того, когда композиция является более подходящим механизмом для расширения существующей функциональности, а не наследованием. - person Contango; 05.06.2013
comment
Итак, если мы добавим еще один метод в класс Child, но все функции Parent по-прежнему имеют смысл в классе Child, нарушит ли он LSP? Поскольку, с одной стороны, мы немного изменили интерфейс для использования Child, с другой стороны, если мы приведем Child как Parent, код, который ожидает, что Parent будет работать нормально. - person Nickolay Kondratyev; 18.06.2013
comment
Это анти-Лисковский пример. Лисков заставляет нас вывести Rectangle из Square. Класс-больше-параметров из класса-меньше-параметров. И вы прекрасно показали, что это плохо. Это действительно хорошая шутка - быть отмеченным как ответ и получить 200 голосов за антилисковский ответ на вопрос лисков. Неужели принцип Лискова - заблуждение? - person Gangnus; 18.10.2015
comment
@Contango А можете ли вы привести пример, в котором наследование является хорошим, согласно цитате в ответе здесь? - person Gangnus; 18.10.2015
comment
Я видел, как наследование работает неправильно. Вот пример. Базовым классом должен быть 3DBoard, а производным классом Board. Доска по-прежнему имеет ось Z: Макс (Z) = Мин (Z) = 1. - person Paulustrious; 05.08.2017
comment
@Nickolay Kondratyev, ага, LSP не ломает, ты сам решил. - person brgs; 12.07.2018
comment
Можно ли решить проблему без использования композиции? Я думаю, что это можно сделать, изменив подпись GetUnits(int x, int y) на GetUnits(Position pos), и то же самое касается других функций. Таким образом, это не нарушит LSP. Поправьте меня если я ошибаюсь. - person du369; 22.04.2020
comment
@Paulustrious это тормозит принцип открыт-закрыт. Что делать, если вам нужна FourDBoard? Вам нужно будет сделать это базовым классом ThreeDBoard, изменив реализацию ThreeDBoard (у вас может даже не быть доступа). - person Jupiter; 19.07.2020
comment
Numpy решил эту точную проблему с помощью ndarray. Трехмерный, двумерный и одномерный массив - все это специализации обобщенного n-мерного массива. Математики поняли это много веков назад. Программисты постепенно изобретают алгебру. - person Adam Acosta; 22.12.2020

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

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

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

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

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

public class Ostrich extends Bird{}

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

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

public class Bird{}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 
person Maysara Alhindi    schedule 04.07.2017
comment
Хороший пример, но что бы вы сделали, если у клиента Bird bird. Вы должны передать объект в FlyingBirds, чтобы использовать fly, что нехорошо, правда? - person Moody; 20.11.2017
comment
Нет. Если у клиента Bird bird, это означает, что он не может использовать fly(). Вот и все. Передача Duck этого факта не меняет. Если клиент имеет FlyingBirds bird, то даже если он получит Duck, он всегда должен работать одинаково. - person Steve Chamaillard; 18.02.2018
comment
Разве это не послужило бы хорошим примером разделения интерфейсов? - person Saharsh; 05.03.2019
comment
Отличный пример Спасибо, человек - person Abdelhadi Abdo; 28.05.2019
comment
Как насчет использования интерфейса «Flyable» (не могу придумать лучшего названия). Таким образом, мы не связываемся с этой жесткой иерархией ... Если мы действительно не знаем, что это нужно. - person Thirdy; 28.05.2019
comment
Ветеринар, который лечит сломанные крылья, увидит Плохой пример как лучший. Некоторые виды нелетающих птиц могут летать своей дорогой (здесь курица - гибрид). Также в процессе эволюции меняются летные способности. Метод fly по умолчанию, допускающий NULL, - хорошее решение, которое позже будет отменено. - person Sławomir Lenart; 31.01.2020
comment
Таким образом, родительский класс должен включать только поведение, которое имеют все его дочерние элементы, или, другими словами, не ограничивать поведение своих дочерних элементов. - person Ari; 25.02.2020
comment
Наконец-то разобрался после часа поисков! - person Boshra N; 17.11.2020

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

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

class Rectangle {
    int getHeight()
    void setHeight(int value) {
        postcondition: width didn’t change
    }
    int getWidth()
    void setWidth(int value) {
        postcondition: height didn’t change
    }
}

class Square extends Rectangle { }

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

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

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

person Konrad Rudolph    schedule 12.09.2008
comment
Отсюда и сложность использования объектно-ориентированного моделирования для моделирования всего, что мы могли бы захотеть смоделировать. - person DrPizza; 30.11.2009
comment
@DrPizza: Совершенно верно. Однако две вещи. Во-первых, такие отношения можно по-прежнему смоделировать в ООП, хотя и не полностью или с использованием более сложных обходных путей (выберите то, что подходит для вашей проблемы). Во-вторых, лучшей альтернативы нет. Другие сопоставления / модели имеют такие же или похожие проблемы. ;-) - person Konrad Rudolph; 30.11.2009
comment
@KonradRudolph, это хороший пример. Как тогда правильно объявить Square? - person ca9163d9; 24.01.2012
comment
@NickW В некоторых случаях (но не в приведенном выше) вы можете просто инвертировать цепочку наследования - логически говоря, 2D-точка - это 3D-точка, где третье измерение не учитывается (или 0 - все точки лежат в одной плоскости в 3D пространство). Но это, конечно, не совсем практично. В общем, это один из случаев, когда наследование действительно не помогает и между объектами не существует естественной связи. Смоделируйте их по отдельности (по крайней мере, я не знаю лучшего способа). - person Konrad Rudolph; 24.01.2012
comment
ООП предназначено для моделирования поведения, а не данных. Ваши классы нарушают инкапсуляцию еще до нарушения LSP. - person Sklivvz; 20.05.2012
comment
@Sklivvz Согласен. Однако было сложно придумать краткий пример, и это стандартный пример LSP (вы найдете его в нескольких книгах). Но да, хотя это обычное дело, это далеко не хорошее ООП. - person Konrad Rudolph; 20.05.2012
comment
Методы setHeight() и setWidth(), вероятно, не должны существовать, потому что это делает класс изменяемым. Однако, если они существуют, можно было бы просто throw NotSupportedException() / NotImplementedException() или что-то подобное. - person Leonid; 03.07.2012
comment
@Leonid В некотором смысле это также нарушает контракт, поскольку изменяет «подпись» (т.е. возможные способы выхода из функции). Некоторые фреймворки (кхм .NET…) обычно используют этот путь, но я считаю его очень плохим дизайном интерфейса. - person Konrad Rudolph; 03.07.2012
comment
Я не понимаю, как здесь нарушен ls LSP. Инвариант утверждает, что getheight и getwidth должны возвращать заданную высоту и заданную ширину, которые всегда истинны, независимо от того, является ли прямоугольник погоды квадратным или прямоугольным. Если клиент устанавливает различную высоту и ширину, он наверняка знает, что объект не квадрат. Точно так же, если клиент знал, что объект квадратный, он установит одинаковую высоту и ширину. Предполагая, что класс прямоугольника имеет две функции, площадь и периметр, функции будут работать правильно, независимо от того, является ли прямоугольник квадратом или нет. Тогда как это нарушает LSP? - person nurabha; 05.06.2013
comment
@nurabha «что всегда верно независимо от погоды, прямоугольник квадратный или прямоугольный» - ложь. С правильно закодированным square это не всегда будет истинным, оба установщика должны сбросить и ширину, и высоту, иначе они не сохранят (подразумеваемые) инварианты квадрата. Другими словами: у вас несовместимая система типов, и вы никогда этого не захотите. - person Konrad Rudolph; 06.06.2013
comment
@nurabha Предположение, что прямоугольник имеет площадь и периметр двух функций, приведет к той же проблеме. SetArea(4); SetPerimeter(10); сделает модель обычного прямоугольника прямоугольником 1x4. Вызовы сделают модель класса Square сначала 2x2, а затем либо изменится на квадрат sqrt (10) xsqrt (10), либо выбросят InvalidOperationException или что-то в этом роде. - person Wolfzoon; 07.08.2016
comment
Если реальных инвариантов нет, как вы могли их нарушить? Как узнать, какие варианты существуют, чтобы их нарушить (или согласовать)? - person iheanyi; 12.08.2016
comment
@iheanyi Вам либо нужно сделать инварианты явными (так называемые утверждения, или использовать систему типов), либо вам нужно знать область, в которой вы работаете. Мы знаем, как определяются прямоугольники и квадраты, поэтому мы знаем, какие инварианты они накладывают. - person Konrad Rudolph; 13.08.2016
comment
Я согласен - либо ограничения, либо инварианты должны быть явными, либо они должны быть навязаны вам системой. Но, исходя из вашего примера, ничто не заставляет меня писать квадратный класс с подразумеваемым инвариантом, о котором вы упомянули. Я полагаю, я предлагаю обновить ваш ответ, чтобы удалить подразумеваемые при обсуждении инвариантов, они являются фактическими ограничениями, которые вызывают проблему. - person iheanyi; 14.08.2016
comment
LSP не должен знать, что делает производный объект, просто производные объекты соблюдают контракт базового класса. Например, в Square может быть SetDepth(int d) => throw exception. Но мне интересно ... прямоугольник - это особый тип квадрата, который не требует H = W? В качестве альтернативы .. Четырехугольник ⇒ Трапеция ⇒ Параллелограмм. - person Paulustrious; 05.08.2017
comment
В качестве альтернативы .._ 1_. В конечном итоге получатся прямоугольник и квадрат. из параллелограмма, хотя был бы параллелограмм ⇒ Ромб ⇒ Квадрат. Получение правильных выводов похоже на проектирование таблиц базы данных. Сделайте ошибку с самого начала, и вы идете к лабиринту - person Paulustrious; 05.08.2017
comment
Будет ли иметь больше смысла для Rectangle унаследовать от типа Square или иметь Square и Rectangle просто родственными классами? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Вы бы избежали этой конкретной проблемы, но отношения на самом деле не имеют смысла: модели наследования отношения «есть-а» и «каждый прямоугольник является квадратом» неверны (и, как следствие, вы столкнетесь с другими проблемы, когда вы не можете просто заменить один на другой). Верно и наоборот («каждый квадрат - прямоугольник»), поэтому так заманчиво написать код, нарушающий LSP. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Итак, не лучше ли оставить их как родственные классы, наследующие от Shape или Parallelogram или чего-то такого? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Ага; чем дольше я работаю в этой области, тем чаще я использую наследование только для интерфейсов и абстрактных базовых классов, а композицию для всего остального. Иногда это немного больше работы (мудрый набор текста), но это позволяет избежать целого ряда проблем и широко используется другими опытными программистами. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Вау, я понятия не имел, однако в моем последнем проекте у меня были проблемы с использованием слишком большого количества наследований. Я понял, что хочу передать некоторым производным классам свойства других производных классов, но у меня не было возможности сделать это. Как вы думаете, только абстрактные классы должны иметь детей по большей части? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan По большей части да. Известно, что это одно из правил влиятельных книг по Эффективному C ++, а более общее правило, Композиция вместо наследования, имеет свое собственное страница Википедии. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Вау, спасибо, я проверю. Стоит ли читать эту книгу при разработке на C #, чтобы изучить структуры и шаблоны проектирования? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan К сожалению, наверное, нет; он очень специфичен для C ++. Однако Эффективный C # Билла Вагнера следует аналогичной схеме. - person Konrad Rudolph; 10.05.2018
comment
@ ca9163d9 Для моделирования Square из Rectangle, если полиморфный подход не является обязательным, я бы поддержал SquareDecorator, который принимает объект Rectangle. Что вы или кто-либо другой думаете об этом? - person Reuel Ribeiro; 11.01.2019
comment
@ReuelRibeiro Это идет в сторону чрезмерной инженерии; в большинстве случаев было бы лучше просто предоставить ортогональные классы, которые могли бы наследовать от Shape. Тем не менее, ваш подход, безусловно, является правильным решением во многих реальных случаях использования. - person Konrad Rudolph; 11.01.2019
comment
Я не уверен, что это хороший пример нарушения LSP. Я понимаю, что это отчасти канонический пример, но утверждение, используемое для доказательства нарушения, не является открытым и закрытым. Да, когда вы читаете код, это кажется очевидным. Я просто установил ширину и высоту, почему высота не та, которую я установил? Но установка ширины и высоты - это две отдельные операции, и, в конце концов, причина, по которой у нас есть методы для установки свойств, а не для раскрытия самих свойств, заключается в том, что мы можем поддерживать наши инварианты, что может привести к побочным эффектам. - person wired_in; 13.10.2020
comment
По сути, я говорю, что, хотя это может быть нарушением, это далеко не очевидно, что это так, и поэтому не следует использовать в качестве примера для людей, пытающихся изучить эту концепцию. - person wired_in; 13.10.2020
comment
@wired_in Я понимаю, откуда вы пришли, но ваш комментарий игнорирует конкретный контекст примера. А именно, мы не просто вызываем какой-либо старый сеттер для любого старого типа объекта. Мы специально назначаем ширину и высоту прямоугольника. И это связано с неявными ожиданиями (неявный контракт). (Неявное) ожидание состоит в том, что изменение ширины не изменяет высоту, и наоборот. Есть причины изменить этот контракт (ведь именно так и делает квадрат!). Но в целом это естественные ожидания. Замена (с квадратом) нарушает эту общность. - person Konrad Rudolph; 13.10.2020
comment
@KonradRudolph Я не игнорировал тот факт, что утверждение было в контексте Rectangle, и я никогда не говорил, что это не было нарушением LSP. Совершенно не очевидно, что существует неявный контракт, в котором говорится, что изменение ширины прямоугольника не может изменить высоту, и наоборот. Поскольку изменение ширины - это отдельная операция от изменения высоты, я не вижу неявного контракта, в котором говорится, что квадрат не может применять свои инварианты, когда ширина или высота изменяются изолированно, как прямоугольник. - person wired_in; 13.10.2020
comment
@KonradRudolph В вашем примере утверждения ширина и высота должны быть установлены одновременно путем объединения двух отдельных операций, но фактический контракт прямоугольника имеет эти операции как отдельные, что делает весь этот пример упражнением по интерпретации, а не конкретное нарушение ЛСП - person wired_in; 13.10.2020
comment
@wired_in «Я не вижу неявного контракта» - вы его не видите, потому что он неявный. Вы можете сделать это явным, добавив к установщикам Rectangle базового класса утверждения, которые гарантируют, что другое значение не будет изменено. Но тот факт, что у квадратов есть другой неявный контракт, именно поэтому это нарушение LSP: оба действительны изолированно, но квадрат не является допустимым подтипом прямоугольника, потому что их неявные контракты не т совместим. - person Konrad Rudolph; 13.10.2020
comment
@wired_in Я добавил явные постусловия в свое объявление класса Rectangle. Но справедливое предупреждение: я могу в конечном итоге удалить их снова, потому что они искусственные и, во всяком случае, уже зафиксированы invariant методом примера. Более того, LSP по своему первоначальному определению также касается неявных инвариантов, а не только явных. Исходный пример без постусловий очень соответствовал исходному формальному определению LSP. - person Konrad Rudolph; 13.10.2020
comment
@KonradRudolph Проблема в том, что неявные инварианты спорны. Они не обязательно высечены в камне. Ваша интерпретация того, что составляет неявный инвариант в этом случае, очень самоуверенна и основана на контексте приложения. - person wired_in; 19.10.2020
comment
@KonradRudolph Я могу утверждать, что, поскольку setHeight() и setWidth() являются независимыми операциями, каждая сама по себе не является нарушением какого-либо инварианта. Только когда вы пытаетесь заставить независимые операции быть одной операцией, можно увидеть ваше представление о том, что вы считаете неявным инвариантом. Так что в лучшем случае это слабый случай нарушения LSP. - person wired_in; 19.10.2020
comment
@wired_in «Это только когда вы пытаетесь заставить независимые операции быть одной операцией» - нет, мы не заставляем их быть одной операцией - это явно не одна операция. Мы навязываем инвариант, то есть утверждения о состоянии объекта. И, опять же, в этом вся суть LSP: инварианты. Речь идет об инвариантах, а не об операциях. - person Konrad Rudolph; 19.10.2020

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

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

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

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

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

[...]

person Phillip Wells    schedule 12.09.2008
comment
Слишком поздно, но я подумал, что это интересная цитата из той статьи: 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. - person user2023861; 11.02.2015
comment
@ user2023861 Вы совершенно правы. Я напишу ответ исходя из этого. - person inf3rno; 09.10.2017

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

Даже тип с неизменяемыми полями все еще имеет инварианты, например Установщики immutable Rectangle ожидают, что размеры будут изменены независимо, но установщики immutable 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 не были контравариантными по отношению к к Ti, тогда подтип Xi - который не будет подтипом Si - может быть назначен Ti.

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

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


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

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

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

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

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

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

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

  • Теоремы Гёделя о неполноте: любая формальная теория, в которой могут быть доказаны все арифметические истины, несовместима.
  • парадокс Рассела: каждое правило членства для набор, который может содержать набор, либо перечисляет определенный тип каждого члена, либо содержит сам себя. Таким образом, множества либо не могут быть расширены, либо являются неограниченной рекурсией. Например, набор всего, что не является чайником, включает себя самого, которое включает себя, которое включает себя и т. Д. Таким образом, правило несовместимо, если оно (может содержать набор и) не перечисляет определенные типы (т. Е. Разрешает все неуказанные типы) и не допускает неограниченного расширения. Это набор наборов, которые не являются членами сами по себе. Эта неспособность быть одновременно непротиворечивой и полностью перечисленной по всем возможным расширениям является теоремой Гёделя о неполноте.
  • Принцип подстановки Лискова: как правило, неразрешимая проблема, является ли какой-либо набор подмножеством другого, т.е. наследование, как правило, неразрешимо.
  • Ссылка Лински: непонятно, что такое вычисление чего-либо, когда оно описывается или воспринимается, то есть восприятие (реальность) не имеет абсолютной точки отсчета.
  • Теорема Коуза: нет внешней точки отсчета, поэтому любой барьер для неограниченных внешних возможностей потерпит неудачу.
  • Второй закон термодинамики: вся Вселенная (замкнутая система, то есть все) стремится к максимальному беспорядку, то есть к максимальным независимым возможностям.
person Shelby Moore III    schedule 26.11.2011
comment
@Shelyby: Вы слишком много перепутали. Вещи не так запутаны, как вы их утверждаете. Многие из ваших теоретических утверждений основаны на неубедительных основаниях, например: «Для существования знания существуют неожиданные возможности .........» И вообще, это неразрешимая проблема, является ли какое-либо множество подмножеством другого, т. Е. наследование вообще неразрешимо ». Вы можете завести отдельный блог по каждому из этих пунктов. В любом случае, ваши утверждения и предположения весьма сомнительны. Нельзя использовать то, чего не осознаешь! - person aknon; 27.12.2013
comment
@aknon У меня есть блог, в котором эти вопросы рассматриваются более подробно. Моя модель TOE бесконечного пространства-времени - это неограниченные частоты. Меня не сбивает с толку то, что рекурсивная индуктивная функция имеет известное начальное значение с бесконечной конечной границей или коиндуктивная функция имеет неизвестное конечное значение и известную начальную границу. Относительность становится проблемой после введения рекурсии. Вот почему завершение по Тьюрингу эквивалентно неограниченной рекурсии. - person Shelby Moore III; 16.03.2014
comment
@ShelbyMooreIII Вы идете слишком во многих направлениях. Это не ответ. - person Soldalma; 09.12.2016
comment
@Soldalma, это ответ. Разве вы не видите его в разделе ответов. Ваш комментарий, потому что он находится в разделе комментариев. - person Shelby Moore III; 23.12.2016
comment
@aknon что касается ваших утверждений о надуманности, я написал в блоге и написано после моего ответа вам в 2014 году. - person Shelby Moore III; 23.12.2016
comment
Нравится ваше смешение с миром scala! - person Ehsan M. Kermani; 28.06.2017

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

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

person Steve Chamaillard    schedule 18.02.2018
comment
Пример не нарушает LSP только до тех пор, пока мы ограничиваем семантику Database::selectQuery для поддержки только подмножества SQL, поддерживаемого всеми механизмами БД. Это вряд ли практично ... Тем не менее, пример все же легче понять, чем большинство других, используемых здесь. - person Palec; 25.02.2018
comment
Я нашел этот ответ самым легким для понимания из всех остальных. - person Malcolm Salvador; 11.02.2019
comment
Практично ли применять LSP к базам данных? Я вижу, что большинство, если не все, операций с базами данных нужно будет обернуть, и они уязвимы для ошибок. Хотя хорошая сторона в том, что API остается прежним, даже если это SQL или NoSQL. - person Sean W; 23.08.2020

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

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

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

  • Никакие новые исключения не должны создаваться в производном классе: если ваш базовый класс выдал исключение ArgumentNullException, тогда вашим подклассам было разрешено генерировать исключения только типа ArgumentNullException или любые исключения, производные от ArgumentNullException. Выброс IndexOutOfRangeException является нарушением Лискова.

  • Невозможно усилить предварительные условия: предположим, что ваш базовый класс работает с членом типа int. Теперь ваш подтип требует, чтобы int был положительным. Это усиленные предварительные условия, и теперь любой код, который раньше отлично работал с отрицательными целыми числами, сломан.

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

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

  • Ограничение истории: при переопределении метода вам не разрешается изменять неизменяемое свойство в базовом классе. Взгляните на этот код, и вы увидите, что Name определен как неизменяемый (частный набор), но 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);
         }
     }
    

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

person Cù Đức Hiếu    schedule 13.08.2016
comment
Я также разработчик 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 контравариантно) msdn.microsoft.com/en-us/library/dd233059 (v = vs.100) .aspx - person LCarter; 05.09.2017
comment
Отличный и конкретный ответ (хотя исходные вопросы были больше о примерах, чем о правилах). - person Mike; 12.06.2018

Короче говоря, давайте оставим прямоугольники, прямоугольники и квадраты, квадраты. Практический пример расширения родительского класса, вы должны СОХРАНИТЬ точный родительский 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 нарушается, когда замена родительского класса на подкласс разрывает контракт API.

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/

person Lukas Lukac    schedule 28.10.2017
comment
Это намного лучший пример. Спасибо! - person Kurt Campher; 14.02.2021

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

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

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

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

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

person Charlie Martin    schedule 08.11.2008
comment
Но ... если вы всегда получаете одно и то же поведение, тогда какой смысл иметь производный класс? - person Leonid; 03.07.2012
comment
Вы упустили момент: это то же самое наблюдаемое поведение. Вы можете, например, заменить что-то с производительностью O (n) на что-то функционально эквивалентное, но с производительностью O (lg n). Или вы можете заменить что-то, что обращается к данным, реализованным с помощью MySQL, и заменить его базой данных в памяти. - person Charlie Martin; 04.07.2012
comment
@Charlie Martin, кодирование для интерфейса, а не для реализации - я это понимаю. Это не уникально для ООП; функциональные языки, такие как Clojure, также способствуют этому. Даже с точки зрения Java или C #, я думаю, что использование интерфейса вместо использования абстрактного класса плюс иерархии классов было бы естественным для примеров, которые вы предоставляете. Python не является строго типизированным и на самом деле не имеет интерфейсов, по крайней мере, явно. Моя трудность в том, что я уже несколько лет занимаюсь ООП, не придерживаясь SOLID. Теперь, когда я наткнулся на это, это кажется ограничивающим и почти противоречащим самому себе. - person Hamish Grubijan; 06.07.2012
comment
Что ж, вам нужно вернуться и проверить оригинальную газету Барбары. reports-archive.adm.cs. cmu.edu/anon/1999/CMU-CS-99-156.ps На самом деле это не указано в терминах интерфейсов, и это логическая связь, которая выполняется (или не поддерживается) в любом языке программирования, который имеет какая-то форма наследования. - person Charlie Martin; 07.07.2012
comment
@HamishGrubijan Я не знаю, кто сказал вам, что Python не является строго типизированным, но они солгали вам (и если вы мне не верите, запустите интерпретатор Python и попробуйте 2 + "2"). Возможно, вы путаете строго типизированный со статическим типом? - person asmeurer; 20.01.2013
comment
@asmeurer Хэмиш не ошибается. Строго типизированный термин не вполне определен; иногда он используется для обозначения какой-то статической проверки типов (значение Хэмиша), иногда для обозначения не выполняет принуждение типа (ваше значение, и, как обычно, только в отношении конкретного случая приведения типа "строка-число" '- Я никогда не видел, чтобы кто-то утверждал, что C слабо типизирован, потому что вы можете умножать целые числа на числа с плавающей запятой, или что Python потому, что вы можете объединить u'hello' с 'world'), а иногда и означать одну из многих других вещей. en.wikipedia.org/wiki/Strong_and_weak_typing - person Mark Amery; 27.12.2013
comment
Что ж, с точки зрения Лискова, это разные, потому что они всегда работают. Если язык поддерживает 2 + '2', он все равно не будет поддерживать 2 + 'two'. - person asmeurer; 27.12.2013
comment
Как ни странно, @MarkAmery, вики-статья, на которую вы ссылаетесь, поддерживает мое использование «строго типизированного». То, что люди использовали это неправильно, не делает определение неправильным. В 1974 году Лисков и Зиллес описали язык со строгой типизацией как язык, в котором всякий раз, когда объект передается от вызывающей функции к вызываемой функции, его тип должен быть совместим с типом, объявленным в вызываемой функции. - person Charlie Martin; 27.12.2013
comment
@CharlieMartin, если я правильно понимаю ваш ответ, то LSP - это возможность заменить любой экземпляр родительского класса экземпляром одного из его дочерних классов без негативных побочных эффектов, верно? - person Daniel; 27.10.2018

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

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

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

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

    • Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more.
    • Более строгие постусловия: они должны производить подмножество вывода, созданного методами супертипа.
  3. Правило свойств: это выходит за рамки отдельных вызовов функций.

    • Invariants : Things that are always true must remain true. Eg. a Set's size is never negative.
    • Эволюционные свойства: обычно что-то связано с неизменяемостью или типом состояний, в которых может находиться объект. Или, может быть, объект только растет и никогда не сжимается, поэтому методы подтипа не должны этого делать.

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

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

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

person snagpaul    schedule 18.12.2016

Проиллюстрируем на 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() { ... }
}

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

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

   void startEngine() { ... }
}

Таким образом, наш класс Car становится более специализированным, при этом соблюдая принцип замещения Лискова.

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

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

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
person Khaled Qasem    schedule 10.02.2019

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

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

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

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

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

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

person Chris Ammerman    schedule 11.09.2008

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

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

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

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

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

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

person avandeursen    schedule 25.03.2013

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

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

person Alireza Rahmani khalili    schedule 16.08.2017

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

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

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

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

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

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

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

person Damien Pollet    schedule 03.04.2009
comment
Эээ, эта формулировка принадлежит Барбаре Лисков. Барбара Лисков, «Абстракция данных и иерархия», Уведомления SIGPLAN, 23,5 (май 1988 г.). Это не слишком сильно, это совершенно верно и не имеет того значения, которое, как вы думаете, имеет. Он сильный, но в нем достаточно силы. - person DrPizza; 30.11.2009
comment
Тогда в реальной жизни очень мало подтипов :) - person Damien Pollet; 09.12.2009
comment
Неизменное поведение не означает, что подтип даст вам точно такие же конкретные значения результата. Это означает, что поведение подтипа соответствует тому, что ожидается от базового типа. Пример: базовый тип Shape может иметь метод draw () и указывать, что этот метод должен отображать форму. Два подтипа формы (например, квадрат и круг) будут реализовывать метод draw (), и результаты будут выглядеть по-разному. Но пока поведение (рендеринг формы) соответствует заданному поведению Shape, тогда Square и Circle будут подтипами Shape в соответствии с LSP. - person SteveT; 11.10.2012

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

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

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

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

Пример:

Ниже приведен классический пример нарушения принципа замещения Лискова. В примере используются 2 класса: прямоугольник и квадрат. Предположим, что объект 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.
    }
}

Вывод:

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

См. Также: Принцип открытого закрытия

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

person GauRang Omar    schedule 21.05.2018

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

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

person johannesMatevosyan    schedule 24.01.2020

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

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

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

Представьте, что однажды клиенту потребуется умение манипулировать квадратами в дополнение к прямоугольникам. Поскольку квадрат является прямоугольником, класс Square должен быть производным от класса 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. Фактически, создание квадрата производного класса вызывает изменения в прямоугольнике базового класса.

person Ivan Porta    schedule 02.03.2020
comment
В методе A() я думаю, вы имели в виду r.Width = 32;, поскольку Rectangle не имеет SetWidth() метода. - person wired_in; 13.10.2020

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

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

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

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

person aknon    schedule 27.12.2013

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

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

person Wouter    schedule 27.07.2015

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

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

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

person Tom Hawtin - tackline    schedule 11.09.2008
comment
Я думаю, что этот пример просто демонстрирует, что наследование с доски не имеет смысла в контексте ThreeDBoard, и все сигнатуры методов бессмысленны с осью Z. - person NotMyself; 11.09.2008

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

person Prasa    schedule 03.05.2016
comment
Приведите примеры кода stackoverflow. - person sebenalern; 03.05.2016

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

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

person inf3rno    schedule 09.10.2017

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();
}
person Zahra.HY    schedule 06.04.2019

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

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

person Ryszard Dżegan    schedule 09.09.2013

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

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

person Raghu Reddy Muttana    schedule 12.08.2016

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

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

Пример - Ковариантные возвращаемые типы в java.

person Ishan Aggarwal    schedule 23.09.2017

Вот отрывок из этого сообщения, в котором хорошо проясняет ситуацию:

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

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

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

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

person Vadim Samokhin    schedule 12.08.2018

В нем говорится, что если C является подтипом E, то E можно заменить объектами типа C без изменения или нарушения поведения программы. Проще говоря, производные классы должны заменять свои родительские классы. Например, если сын фермера - фермер, то он может работать вместо своего отца, но если сын фермера играет в крикет, он не может работать вместо своего отца. отец.

Пример нарушения:

public class Plane{

  public void startEngine(){}      

}        
public class FighterJet extends Plane{}
    
public class PaperPlane extends Plane{}

В данном примере классы FighterPlane и PaperPlane расширяют класс Plane, который содержит метод startEngine(). Итак, ясно, что FighterPlane может запустить двигатель, но PaperPlane не может, поэтому он ломается LSP.

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

Уважаемый пример:

public class Plane{ 
} 
public class RealPlane{

  public void startEngine(){} 

}
public class FighterJet extends RealPlane{} 
public class PaperPlane extends Plane{}
person Sarmad Sohail    schedule 22.11.2020

[SOLID]

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

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

* Предусловие и постусловие: function (method) types [Тип функции Swift. Функция Swift и метод]

//C1 <- C2 <- C3

class C1 {}
class C2: C1 {}
class C3: C2 {}
  • Предварительные условия (например, функция parameter type) могут быть такими же или более слабыми. (стремится к - ›C1)

  • Постусловия (например, функция returned type) могут быть такими же или более сильными (стремится к - ›C3)

  • Инвариантная переменная супертипа [About] должна оставаться неизменной.

Быстрый

class A {
    func foo(a: C2) -> C2 {
        return C2()
    }
}

class B: A {
    override func foo(a: C1) -> C3 {
        return C3()
    }
}

Джава

class A {
    public C2 foo(C2 a) {
        return new C2();
    }
}

class B extends A {

    @Override
    public C3 foo(C2 a) { //You are available pass only C2 as parameter
        return new C3();
    }
}

Подтип

  • Sybtype не должен требовать от вызывающего больше, чем супертип
  • Sybtype не должен открывать для вызывающего абонента меньше супертипа

Контравариантность типов аргументов и ковариация возвращаемого типа.

  1. Контравариантность аргументов метода в подтипе.
  2. Ковариация возвращаемых типов в подтипе.
  3. Никакие новые исключения не должны генерироваться методами подтипа, за исключением случаев, когда эти исключения сами являются подтипами исключений, генерируемых методами супертипа.

[Дисперсия, ковариация, контравариантность]

person yoAlex5    schedule 11.11.2020

Попробую, рассмотрим интерфейс:

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();

Примеры взяты здесь

person prady00    schedule 26.04.2019
comment
Плохой пример. Earth является экземпляром plant, почему он должен быть производным от него? - person zar; 03.05.2019