Что может пойти не так, если нарушится принцип подстановки Лискова?

27

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

Фанат
источник
6
Что может пойти не так, если вы не будете следовать LSP? В худшем случае: вы в конечном итоге вызываете Код-тулху! ;)
FrustratedWithFormsDesigner
1
Как автор этого оригинального вопроса, я должен добавить, что это был довольно академический вопрос. Хотя нарушения могут привести к ошибкам в коде, у меня никогда не было серьезных ошибок или проблем с обслуживанием, которые я могу отнести к нарушению LSP.
Пол Т Дэвис
2
@Paul Так что у вас никогда не было проблем с вашими программами из-за запутанных ОО-иерархий (которые вы сами не проектировали, но, возможно, пришлось расширять), когда контракты нарушались влево и вправо людьми, которые не знали цели базового класса начать с? Я тебе завидую! :)
Андрес Ф.
@PaulTDavies серьезность последствий зависит от того, имеют ли пользователи (программисты, которые используют библиотеку) детальные знания о реализации библиотеки (то есть имеют доступ и знакомы с кодом библиотеки.) В конечном счете пользователи будут ставить десятки условных проверок или создавать оболочки вокруг библиотеки, чтобы учесть не-LSP (специфичное для класса поведение). Худший вариант развития событий произойдет, если библиотека будет коммерческим продуктом с закрытым исходным кодом.
Rwong
@ Andres и Rwong, пожалуйста, проиллюстрируйте эти проблемы с ответом. Принятый ответ в значительной степени поддерживает Пола Дэвиса в том, что последствия кажутся незначительными (исключение), которое будет быстро замечено и исправлено, если у вас есть хороший компилятор, статический анализатор или минимальный модульный тест.
user949300

Ответы:

31

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

Теперь при вызове Close () для Задачи существует вероятность того, что вызов завершится неудачей, если это ProjectTask с запущенным состоянием, а если нет, если это базовая Задача.

Представьте, если вы будете:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

В этом методе иногда вызывается вызов .Close (), поэтому теперь, основываясь на конкретной реализации производного типа, вы должны изменить способ поведения этого метода по сравнению с тем, как этот метод был бы написан, если бы в Task не было подтипов, которые могли бы быть передал этот метод.

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

Джимми Хоффа
источник
Означает ли это, что у дочернего класса не может быть своих собственных открытых методов, которые не объявлены в родительском классе?
Сонго
@Songo: Не обязательно: это возможно, но эти методы «недоступны» из базового указателя (или ссылки, или переменной, или того языка, который вы используете для его вызова), и вам нужна некоторая информация о типе во время выполнения, чтобы запросить тип объекта прежде чем вы можете вызвать эти функции. Но это вопрос, тесно связанный с синтаксисом и семантикой языков.
Эмилио Гаравалья
2
Нет. Это для случаев, когда на дочерний класс ссылаются, как если бы это был тип родительского класса, и в этом случае члены, которые не объявлены в родительском классе, недоступны.
Chewy Gumball
1
@Phil Yep; это определение тесной связи: изменение одной вещи вызывает изменения в других вещах. Слабосвязанный класс может изменить свою реализацию, не требуя изменения кода вне его. Вот почему контракты хороши, они подсказывают вам, как не требовать изменений для потребителей вашего объекта: выполняйте контракт, и потребители не нуждаются в модификации, таким образом достигается слабая связь. Когда вашим потребителям нужно писать код для вашей реализации, а не для вашего контракта, это является жесткой связью и требуется при нарушении LSP.
Джимми Хоффа
1
@ user949300 Успех любого программного обеспечения для выполнения своей работы не является мерой его качества, долгосрочных или краткосрочных затрат. Принципы проектирования - это попытки выработать руководящие принципы для сокращения долгосрочных затрат на программное обеспечение, а не для того, чтобы программное обеспечение «работало». Люди могут следовать всем принципам, которые они хотят, но при этом не могут реализовать рабочее решение, или не следовать ни одному из них и внедрять рабочее решение. Хотя коллекции java могут работать для многих людей, это не означает, что затраты на работу с ними в долгосрочной перспективе настолько дешевы, насколько это возможно.
Джимми Хоффа
13

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

LSP в википедии говорится

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

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

Zavior
источник
1
Можете ли вы привести какие-либо конкретные примеры, чтобы продемонстрировать это?
Марк Бут
1
@MarkBooth Задача круг-эллипс / квадрат-прямоугольник может быть полезна для ее демонстрации; Статья в Википедии - хорошее место для начала: en.wikipedia.org/wiki/Circle-ellipse_problem
Эд Гастингс,
7

Рассмотрим классический случай из анналов вопросов на собеседование: вы получили Circle из Ellipse. Зачем? Потому что круг - это эллипс, конечно!

Кроме ... Эллипс имеет две функции:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

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

  1. После вызова set_alpha_radius или set_beta_radius оба устанавливаются на одну и ту же сумму.
  2. После вызова set_alpha_radius или set_beta_radius объект больше не является кругом.

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

some_function(Ellipse byref e)

Представьте, что some_function вызывает e.set_alpha_radius. Но поскольку e действительно был Кругом, у него, к удивлению, также установлен его бета-радиус.

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

Каз Дракон
источник
1
Я думаю, что вы можете столкнуться с проблемами, если вы используете изменяемые объекты. Круг - это тоже эллипс. Но если вы замените эллипс, который также является кругом, другим эллипсом (что вы и делаете, используя метод сеттера), нет гарантии, что новый эллипс также будет кругом (круги - это правильное подмножество эллипсов).
Джорджио
2
В чисто функциональном мире (с неизменяемыми объектами) метод set_alpha_radius (d) будет иметь тип эллипса возвращаемого типа (как в эллипсе, так и в классе круга).
Джорджио
@ Джорджио Да, я должен был упомянуть, что эта проблема возникает только с изменяемыми объектами.
Kaz Dragon
@KazDragon: Зачем кому-то заменять эллипс объектом круга, если мы знаем, что эллипс НЕ является кругом? Если кто-то делает это, у него нет правильного понимания сущностей, которые он пытается смоделировать. Но, допуская такую ​​замену, разве мы не поощряем свободное понимание базовой системы, которую мы пытаемся смоделировать в нашем программном обеспечении, и таким образом создаем плохое программное обеспечение в действительности?
индивидуалист
@ Maverick Я полагаю, что вы прочитали отношения, которые я описал в обратном направлении. Предложенные отношения - это наоборот: круг - это эллипс. В частности, круг - это эллипс, в котором радиусы альфа и бета идентичны. Таким образом, можно ожидать, что любая функция, ожидающая эллипс в качестве параметра, может в равной степени занять круг. Рассмотрим расчета_области (эллипса). Переходя к этому кругу, вы получите тот же результат. Но проблема в том, что поведение мутационных функций Эллипса не может заменить поведение в Круге.
Каз Дракон
6

По словам непрофессионала:

В вашем коде будет очень много предложений CASE / switch .

Каждое из этих предложений CASE / switch будет время от времени нуждаться в новых случаях, что означает, что кодовая база не так масштабируема и обслуживаема, как должна быть.

LSP позволяет коду работать как аппаратное обеспечение:

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

Тулаинс Кордова
источник
2
-1: все вокруг плохой ответ
Томас Эдинг
3
@ Томас, я не согласен. Это хорошая аналогия. Он говорит о том, чтобы не нарушать ожидания, о чем и LSP. (хотя часть о корпусе / переключателе немного слабая, я согласен)
Андрес Ф.
2
А потом Apple сломала LSP, сменив разъемы. Этот ответ живет дальше.
Маг
Я не понимаю, что операторы switch имеют отношение к LSP. если вы имеете в виду переключение, typeof(someObject)чтобы решить, что вам «разрешено делать», то конечно, но это совсем другой паттерн.
Сара
Резкое сокращение количества операторов переключения является желательным побочным эффектом LSP. Поскольку объекты могут обозначать любой другой объект, который расширяет тот же интерфейс, не нужно заботиться о специальных случаях.
Тулаинс Кордова
1

привести пример из реальной жизни с помощью Java UndoManager

он наследует от того, AbstractUndoableEditчей контракт указывает, что он имеет 2 состояния (отменено и переделано) и может переходить между ними с помощью одного вызова undo()иredo()

однако UndoManager имеет больше состояний и действует как буфер отмены (каждый вызов undoотменяет некоторые, но не все изменения, ослабляя постусловие)

это приводит к гипотетической ситуации, когда вы добавляете UndoManager в CompoundEdit перед вызовом, а end()затем вызываете отмену для этого CompoundEdit, что приведет к его вызову undo()при каждом редактировании, после того как ваши изменения будут частично отменены

Я свернул свой собственный, UndoManagerчтобы избежать этого (я, вероятно, должен переименовать его, UndoBufferхотя)

чокнутый урод
источник
1

Пример: вы работаете с UI-фреймворком и создаете свой собственный пользовательский UI-элемент управления путем создания подкласса Controlбазового класса. ControlБазовый класс определяет метод , getSubControls()который должен возвращать коллекцию вложенных элементов управления (если таковые имеются). Но вы переопределяете метод, чтобы фактически вернуть список дат рождения президентов США.

Так что может пойти не так с этим? Очевидно, что рендеринг элемента управления не удастся, так как вы не возвращаете список элементов управления, как ожидалось. Скорее всего, пользовательский интерфейс будет сбой. Вы нарушаете договор, которого должны придерживаться подклассы Control.

JacquesB
источник
0

Вы также можете посмотреть на это с точки зрения моделирования. Когда вы говорите, что экземпляр класса Aтакже является экземпляром класса, Bвы подразумеваете, что «наблюдаемое поведение экземпляра класса Aтакже может быть классифицировано как наблюдаемое поведение экземпляра класса B» (это возможно только в том случае, если класс Bменее специфичен, чем класс A.)

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

Как сделать коробку с тегом: «Эта коробка содержит только синие шары», а затем бросить в нее красный шар. Какая польза от такого тега, если он показывает неверную информацию?

Джорджио
источник
0

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

У меня есть Class A, что вытекает из Class B. Class Aи Class Bподелиться кучей свойств, которые Class Aпереопределяют с его собственной реализацией. Установка или получение Class Aсвойства по-разному влияет на установку или получение точно такого же свойства Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

Если оставить в стороне тот факт, что это очень ужасный способ перевода в .NET, у этого кода есть ряд других проблем.

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

Код использует объекты обоих Class Aи Class B, поэтому я не могу просто сделать Class Aабстракцию, чтобы заставить людей использовать Class B.

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

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

Чтобы это исправить, мне нужно создать NameTranslatedсвойство, которое будет являться Class Bверсией Nameсвойства и очень, очень тщательно изменять каждую ссылку на производное Nameсвойство, чтобы использовать мое новое NameTranslatedсвойство. Однако, если хотя бы одна из этих ссылок неверна, все приложение может взорваться.

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

Стивен
источник
Что произойдет, если в производном классе вы замаскируете унаследованное свойство другим видом с тем же именем [например, вложенный класс] и создадите новые идентификаторы BaseNameи TranslatedNameполучите доступ как к стилю класса A, так Nameи к значению класса B? Тогда любая попытка доступа Nameк переменной типа Bбудет отклонена с ошибкой компилятора, так что вы можете убедиться, что все ссылки были преобразованы в одну из других форм.
суперкат
Я больше не работаю в этом месте. Это было бы очень неловко исправить. :-)
Стивен
-4

Если вы хотите почувствовать проблему нарушения LSP, подумайте, что произойдет, если у вас есть только .dll / .jar базового класса (без исходного кода) и вам нужно создать новый производный класс. Вы никогда не сможете выполнить эту задачу.

SGUD
источник
1
Это просто открывает больше вопросов, а не является ответом.
Франк