Стек, расширяющий LinkedList. Нарушение принципа подстановки Лискова?

13

Существует класс LinkedList с такими функциями, как add_first (), add_last (), add_after (), remove_first (), remove_last () и remove ()

Теперь есть класс Stack, который предоставляет такие функции, как push (), pop (), peek () или top (), и для реализации этих методов он расширяет методы класса LinkedList. Это нарушение принципа подстановки Лискова?

Например, рассмотрим случай add_after () для добавления узла в n-ю позицию связанного списка. Это можно сделать в базовом классе, но не в классе Stack. Постусловия здесь ослаблены, или вы модифицируете метод add_after () для добавления в верхнюю часть стека?

Кроме того, если не нарушение, это плохой дизайн? И как бы вы реализовали функциональность Stack с помощью класса LinkedList?

jxvmiranda
источник
6
Вопрос Что может быть недостатком для определения класса как подкласса списка самого себя? спрашивает о немного другой проблеме, но большинство ответов также применимы и здесь: вам лучше создать стек со списком в качестве частного члена, а не наследовать от списка.
Амон
7
Да, это плохой дизайн, поскольку он не позволит вам изменить внутреннее представление стека на что-либо, кроме связанного списка (например, массива). Вы также выставляете операции, которые стеки обычно не поддерживают. Используйте композицию вместо наследования.
Ли
2
Вы хотите продлить LinkedList? Возможно, вы захотите прислушаться к совету некоего Джошуа Блоха (который сыграл главную роль в разработке инфраструктуры коллекций Java), когда он сказал: «Предпочитаю композицию над наследованием». Может быть, есть LinkedListвнутренний класс стека, но на самом деле не расширять его?
CorsiKa
1
Я бы сказал, что он не может удовлетворить принципу подстановки Лискова, потому что «стек - это своего рода связанный список» - не верное утверждение. «Стек» - это действительно интерфейс (я имею в виду концептуально, а не конструкцию Java). Стек может быть реализован с помощью связанного списка. Как уже говорили другие, вы бы использовали личный элемент данных типа, LinkedListкоторый выполняет тяжелую работу за кулисами (композиция). Таким образом, пользователи вашего кода не могут случайно использовать стек, где им нужен список, или наоборот.
Jasmijn
1
Кажется, что было бы разумнее, было бы наоборот. Если бы я делал это, у меня, вероятно, был бы класс связанного списка, реализующий интерфейс «стека», поскольку он полностью способен на это. В противном случае это неправильный подход, поскольку стек - это концепция, а связанный список - это реализация, которая может выступать в качестве стека, помимо прочего.
Vality

Ответы:

31

Теперь есть класс Stack, который предоставляет такие функции, как push (), pop (), peek () или top (), и для реализации этих методов он расширяет методы класса LinkedList. Это нарушение принципа подстановки Лискова?

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

Например, рассмотрим случай add_after () для добавления узла в n-ю позицию связанного списка. Это можно сделать в базовом классе, но не в классе Stack.

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

Постусловия здесь ослаблены, или вы модифицируете метод add_after () для добавления в верхнюю часть стека?

Изменение add_after()метода таким способом является нарушением правила истории (самое важное из правил!) И, таким образом, не помогает устранить нарушение LSP.

И как бы вы реализовали функциональность Stack с помощью класса LinkedList?

Используя композицию.

ПРИМЕЧАНИЕ. Все, что я написал выше, относится только к языкам, которые путают подтипы и подклассы! LSP - это подтип , а не подкласс. В языке , который не путает два, это было бы вполне приемлемо , чтобы сделать Stackподкласс LinkedList, до тех пор , пока вы не сделать его подтип из LinkedList.

Йорг Миттаг
источник
9
Что такое правило истории? Я не смог найти его в интернете.
Западло
10
При манипулировании объектом подтипа и наблюдении за ним с помощью методов супертипа, должно быть невозможным наблюдать историю, которую также нельзя наблюдать с объектом супертипа. Это правило делает LSP применимым к нефункциональным языкам. Все остальное уже было известно задолго до этого. Правила до и после условия являются переформулировками правил ко- и контравариантности для типов функций. Правило истории делает все это применимым к изменяемым данным. Без этого LSP был бы полезен только для чисто функциональных языков.
Jörg W Mittag
2
Я думаю, что объяснение в статье в Википедии довольно хорошее: wikipedia.org/wiki/Liskov_substitution_principle Но, как всегда, вы действительно должны прочитать исходный текст.
Йорг Миттаг
@ JörgWMittag: Хотя я считаю целесообразным, чтобы типы включали в свои контрактные гарантии то, что «история» будет или не будет наблюдаться, я не думаю, что необходимо, чтобы каждый супертип был способен производить каждую последовательность наблюдений это может быть возможно для объекта подтипа - просто, что супертип документирует возможность того, что потребители супертипа могут наблюдать такие последовательности.
суперкат
1

Поскольку все уже было рассмотрено Йоргом Миттагом, я просто уточню следующую часть:

Постусловия здесь ослаблены, или вы модифицируете метод add_after () для добавления в верхнюю часть стека?

В основном, когда возникает вопрос, нарушает ли какая-либо иерархия LSP, это зависит от того, какой контракт вы заключаете. Итак, какой контракт add_afterимеет? Если это звучит, как бы странно это ни звучало, например, «Добавить узел в n-й позиции или наверху», то все в порядке, постусловие выполнено, LSP не нарушается. В противном случае это нарушение LSP.

Западло
источник