А если он наследуется от базового класса это не получается автоматически? Привожу пример есть класс «млекопитающие» - все наследники получают умение дышать, есть, издавать звуки, размножаться и т.д.
Привожу пример есть класс «млекопитающие» - все наследники получают умение дышать, есть, издавать звуки, размножаться и т.д.
Ты в наследнике можешь в принципе добавить публичный метод, которого в базовом классе нет. Вот это по Лисков - плохо. Потому что у тебя подкласс делает что-то, что базовый класс не делает. И т.о. нельзя подставить подкласс вместо базового (или наоборот) в произвольное выражение.
Потому что у тебя подкласс делает что-то, что базовый класс не делает.
А почему это плохо и нарушается этот принцип? Разве наследование не придумали специально для этого, расширять базовые классы и уменьшать дублирование кода в программе? Зачем в подклассе «корова» заново реализовывать функцию «дышать» из млекопитающего?
Там очень сложно и слишком подробно, я С++ только начала. Не думаю, что нужно слушать больше часа, чтобы понять принцип. Вот есть фраза LSP «гарантирует семантическую интероперабельность (interoperability) типов в иерархии». А нафиг мне такие гарантии, если корову собакой в иерархии типов «млекопитающие» заменить нельзя?
Разве наследование не придумали специально для этого, расширять базовые классы
Не совсем. Наследование придумали, чтобы подтипы могли делать что-то специфическое. Т.е. на «издать звук» корова делает «му», а собака «гав».
Зачем в подклассе «корова» заново реализовывать функцию «дышать» из млекопитающего?
А где я сказал, что её нужно делать заново? Я сказал, что корова не должна иметь никаких дополнительных возможностей, которых нет у базового «млекопитающее».
почему это плохо
Потому что тогда объекты иерархии не будут взаимозаменяемые. Допустим все «животные» имеют метод «идти». Но некоторые подклассы (не все) имеют ещё метод «рыть». Тогда в условном алгоритме «идти вперёд, если препятствие, то рыть» получится облом, т.к. рыть могут не все и нужно перед «рыть» сначала проверять, а может ли данный конкретный объект рыть. И вот эта проверка в принципе ломает ООП, т.к. оно изначально задумано, чтобы таких явных проверок избежать и дать объектам самим решать что делать.
можешь в принципе добавить публичный метод, которого в базовом классе нет. ... т.о. нельзя подставить подкласс вместо базового
Почему это? Вызывающий код же не знает про твой новый метод. Что он есть, что его нет. Вот если ты переопределишь существующий метод таким образом, что он перестанет делать то, что от него ожидает вызывающий код...
Но некоторые подклассы (не все) имеют ещё метод «рыть». Тогда в условном алгоритме «идти вперёд, если препятствие, то рыть»
Так этот алгоритм не применим для класса «животное». В нём ведь метод «рыть» вообще не определён.
Поэтому расширение в общем случае LSP разрешает. Запрещает изменение поведения в подклассах. Например, если для животного «перемещаться вперед» = «перемещаться вдоль земли в направлении взгляда», а птицу мы унаследуем от животного, но для неё будет «перемещаться вперед» = «лететь в направлении взгляда», то LSP нарушится. Потому что алгоритмы, работавшие для любого животного, перестанут работать, если животное — птица.
Кстати, есть исключение. Если язык — smalltalk или ruby, то метод на выполнение у объекта можно запросить любой. Если не определён конкретный, то будет выполнен метод method_missing. И в этом случае получается, что поведение производного класса, в котором метод определили существенно отличается от поведения базового класса при вызове этого метода.
Вот именно. Поэтому если у каких-то животных добавить этот метод, то уже нельзя просто любых животных подставлять в любой алгоритм.
Запрещает изменение поведения в подклассах
Понятие «поведение» слишком философское. Я сторонник того, чтобы рассматривать «поведение» в контексте синтаксиса языка. Т.е. интерфейсов, контрактов и т.п. явно заданного «поведения».
Потому что алгоритмы, работавшие для любого животного, перестанут работать, если животное — птица
Тогда это плохие алгоритмы, которые полагались на неявно заданный интерфейс. За неявность интерфейса тоже нужно выдавать леща.
Он в корне не прав. Объект от унаследованного класса должен уметь замещать объект от основного класса, всё. Расширяй интейфейс сколько угодно, принципу это не противоречит.
Запросто можно деться. Если язык что-то не позволяет задать непосредственно в рабочем коде, то на этот случай есть тесты, которые по сути являются offline контрактами.
Если про наивность, есть куча программных объектов, нарушающих LSP. От символических ссылок в файловой системе до «текстовое поле с кнопкой» как расширение «текстовое поле». И про каждый из них есть описание, чего не надо «по наивности» делать.
С математической точки зрения, ещё и дополнительные поля добавлять нельзя, иначе базовый тип имеет свойство «можно сериализовать в 8 байт», а производный, внезапно, не имеет.
Но речь-то ведь идёт о подстановке подкласса в место, где ожидается его родитель
А как ты собрался отличать родителя от особых подклассов? Вот ты добавил некоторым подклассам новый метод. Как объявить функцию обрабатывающую такие подклассы?
Вот именно. Поэтому если у каких-то животных добавить этот метод, то уже нельзя просто любых животных подставлять в любой алгоритм.
В смысле? Если у тебя объект А имеет тип Животное, то ты просто не сможешь взывать А.рыть(), так как у типа Животное этого метода нет (будет ошибка компиляции). Если объект А имеет тип Крот (унаследованный от Животное), то А.рыть() вызвать можно, но на других животных это никак не влияет.
нельзя просто любых животных подставлять в любой алгоритм
Если алгоритм требует тип аргумента Животное, то метод «рыть» всё равно вызвать не получится, даже если у каких-то подклассов он и определён.
Liskov substitution - это такой ООП-шный аналог Дейкстровских «Доводов против оператора GOTO»: беспроигрышный повод для холиваров с участием религиозных фанатиков с обоих сторон, а на деле просто инструмент, пригодный в одних случаях и малопригодный в других (без сравнения частоты возникновения этих случаев).
А как ты собрался отличать родителя от особых подклассов?
Зачем мне это делать? С точки зрения вызывающего кода не должно быть никакой разницы, родителя ты передал или потомка. Если ожидается ПлоскаяФигура, можно передать туда ПлоскаяФигура, его потомка Треугольник или потомка потомка РавнобедренныйТреугольник, и наличие лишних методов не должно ничего сломать — потому что вызывающий код может рассчитывать только на методы ПлоскаяФигура, которые у всех его потомков 1) должны быть и 2) должны делать the right thing.
Запросто можно деться. Если язык что-то не позволяет задать непосредственно в рабочем коде, то на этот случай есть тесты, которые по сути являются offline контрактами.
Вот считай, что направление взгляда и движения такими тестами и проверялось. Кроме того, для любой используемой системы, фактическими тестами является её использование. Если, например, файловый путь «а/b/../c/..» всегда (до появления символических ссылок) можно было сократить да «a», то символические ссылки нарушают интерфейс.
Если в основном классе «млекопитающие» определен метод «бежать», а дельфин бегать не умеет, то нужно строить отдельную базовую иерархию «млекопитающие-не-умеющие-бегать»?
Нет. Это критерий необходимости/возможности наследования для типизированного ООП. Если тебе где-то надо в алгоритм, обрабатывающий объекты типа А передать объект типа Б, значит тип Б обязан быть наследником типа А. А чтобы он мог быть наследником, должен соблюдаться LSP (то есть он должен быть пригоден не только для конкретного алгоритма, в которые его хочешь запихнуть, а для произвольного алгоритма).
А «Доводы против оператора GOTO» теперь называются «Доводы против наследования». Мол, случаи, когда эта ересь необходима, исключительно редки, поэтому надо её вообще выпилить из языка. Уже есть языки с «ООП без наследования».
Затем что ты какой-то тип у функции должен указать. У тебя есть Base, и наследники A,B,C,D. Из них C и D имеют новый метод thatThing(). Нужно написать функцию, которая работает только с C и D. Твои предложения?
Да не получится напрямую, придётся кастовать. О чём и речь.
Кастовать в программах, соответствующих LSP нельзя.
На каждый подкласс пилить метод через перегрузку? Прикручивать шаблон?
Если у тебя несколько классов имеют общий метод, этот метод надо определять в интерфейсе или общем предке этих подклассов. Тогда алгоритмы, которые могут использовать этот метод будут указывать в качестве типа этот интерфейс или общий предок с методом.
Итог - если добавляешь новый метод, то использовать можно только через жопу.
Это хорошо, что не ООПшник. Интерфейс - это не ведро каких-то сигнатур, это набор ожиданий. В интерфейс входит не только то, что класс Document реализует метод void upload(URL) throws IOError, но и, например, то, что всегда выполняется за конечное время, не изменяет состояние самого объекта, и, допустим, как и весь код проекта, следует стандарту безопасности АТАТА2020. Интерфейс - это совокупность ожиданий того, кто использует объект. Формализовать их - великое, обычно несбыточное дело.
Не так. В классе «млекопитающие» метода «бегать» быть тогда не должно (не все умеют). А у него сделать подкласс «бегающие-млекопитающие» с методом «бегать».
По идее нет, все нормально. Главное не делать сюрпризов в методах, которые есть в базовом классе.
Вообще сам принцип это просто идея, плюс небольшая помощь в реализации этих идей в виде того как делается наследование в языках программирование. Компилятор по крайней мере следит чтобы в наследовании типы в сигнатурах не становились шире.
Но это лишь помощь программисту, а не гарантия. Подкласс Dog класса или интерфейса Animal легко может в методе walk() отформатировать вам винт. Это будет нарушение принципа, его идеи. Компилятор от такого не защищает
Винт? Чтобы нарушить LSP, достаточно унаследовать от собаки хромую собаку, которая ходит, но поскуливая, а потом жаловаться, что собака при ходьбе скулит. Да куда там, достаточно сделать собаку, которая ходит слишком медленно. Если от этого в кроется, скажем, race condition (вторая собака будет съедать весь её корм, и первая, хромая, умрёт с голодухи), то знай - виновато нарушение LSP. От собаки ожидалось, что она идёт жрать за разумное время? Тогда хромая собака - не собака.
Разве вся полезность ООП не связана с наследованием?
Когда-то вся полезная деятельность программ была связана с конструкцией IF .. GOTO. А потом пришёл Дейкстра.
Так и с ООП. В Go есть ООП и нет наследования. В Rust тоже. Точнее есть только для интерфейсов.
В 1С нет наследования (но там утиная типизация, поэтому можно реализовать вручную).