Comments 38
Мне вот кажется, что проще всего объяснить LSP на примере от противного, который мы можем найти в JVM.
Есть интерфейс java.utils.List
, у него есть метод add
. И должно это все работать так:
List<String> list = new ArrayList<>();
list.add("x");
list.contains("x") //true
Однако можно сделать так:
List<String> list = Collections.unmodifiableList(new ArrayList<>());
list.add("x"); // низззяяя, UnsupportedOperationException
или так
List<String> list = Collections.emptyList()
list.add("x"); // низззяяя, UnsupportedOperationException
То есть у нас на руках тип List, а работать как с листом мы с ним не можем. Косямба? Косямба. Вот LSP говорит, что так делать не надо.
Да и с расширением функциональности тоже не всё гладко. Всегда переезжать базовый метод или добалять новый? И как потом не запутаться какой вызывать?
Да, с одной стороны проще, а с другой сложнее. Все зависит от особенностей восприятия материала конкретным человеком. В этой статье я все описал максимально абстрактно, не привязываясь к конкретным инструментам языка, объяснил так, как хотел бы, чтобы объяснили мне. Плюс я постарался, чтобы у читателя возникло общее понимание информации, которое он в дальнейшем сможет проиллюстрировать примером, в частности вашим. А так, все равно в процессе познания приходится копаться в десятках источников) И вообще эта статья про запоминалку)
Если list.add("x") недоступен, значит это нечно не выполняет соглашение интерфейса List. Если при этом компилятор считает считает, что интерфейс List соблюдён, это баг компилятора.
Ну, вообще тут косяк зарыт гораздо глубже. Ну нет в Java изначально интерфейсов для иммутабл коллекций, как скажем есть в скале. Поэтому у вас формально-то List, а фактически некий ...ImmutableList, но бедность системы типов языка не позволяет это выразить. А так бы оно просто не скомпилировалось.
Причём, если стремиться сохранить обратную совместимость, текущую иерархию уже никак не расширить. Впихнуть ReadableIterator, ReadableIterable, ReadableCollection, ReadableList, Readable* то можно, но вот что делать с утилитарными классами и методами(Collections, List::of), которые возвращают экземпляры List, Set, Map и т. д. это большой вопрос.
Ну да, в том числе. В принципе, я пытался и пытаюсь в текущем проекте пользоваться скажем классами из Vavr, где к примеру у List есть сразу методы map или filter, и удобные конструкторы, а заодно TupleN. В какой-то степени так жить (с другой иерархией коллекций) можно — но только пока у вас мало используется чей-то чужой API, где все интерфейсы классические. Тогда начинается везде toJavaMap, а имплиситов как в скале — не завезли...
Ну если быть честным, то в документации add к интерфейсу List написано следующее.
* @throws UnsupportedOperationException if the {@code add} operation
* is not supported by this list
конечно документация такой себе контракт, но все же контракт.
И по хорошему клиентский код принимающий List должен учитывать что add может выбросить UnsupportedOperationException
Метод add должен добавить элемент в коллекцию или не существовать. Каким именно образом его убрать, это уже детали реализации.
Почему бы не сделать какой-то ImmutableList, который имеет доступ только для итерации списка, но не имеет методов для редактирования. Далее - его потомок, уже с такими методами. Или добавить реализацию всех методов, но скрыть их за интерфейсами.
А так, это примерно как иметь лампочку и выключатель этой лампочки, который, при нажатии, подводит ток к тебе. Довольно непредсказуемое поведение.
Должен кому? Я указал на документацию к интерфейсу которую стоит учитывать если хочешь работать с листами с учетом LSP.
А вам указали на то, что с такой документацией этот интерфейс нафиг не нужен
Так не нужен, что c 1998 года используется.
Но он используется-то не благодаря замечательному дизайну, а за отсутствием альтернатив.
Вот не могу представить код, который бы принимал List, вызывал у него метод add и не забывал обработать исключение в случае если add не поддерживается. Ну никто же так не делает!
А зачем мне обрабатывать исключение? Если мне нужно мутировать содержимое для вызывающего код, то выброс исключения UnsupportedOperationException будет очевидным fail-fast поведением.
Если мне нужно мутировать содержимое в рамках моего кода то все и так давно используют (неявно) подход с копированием коллекции ака
unknownList.stream() .map() .filter()
Да и вообще в современном конкаренси ФП мире бест практис писать чистые функции. А это значит без модификации исходной коллекции и сигнатура в таком случае будет.
<T> List<T> addSomethingToList(List<T> list)
Я бы очень хотел конечно чтобы в Java были отдельные интерфейсы для мьютабл и имьютабл коллекций. Чтобы были и Intersection Types и Union Types и вообще чтобы все было по теории категорий и ругалось на этапе статического анализа. Но к сожалению реальность немного иная.
Вы уж определитесь, или метод нужен и используется с 1998 года, или все давно на другой подход перешли. Так что кроме первого возражения остальные не имеет отношения к предмету спора.
Что же до первого возражения — то вы просто сводите проблему к ней же самой, только теперь проблеме с исключением которое невозможно продуктивно обработать подвержен уже ваш метод.
Ещё раз, зачем объявлять функционал, который потенциально не работает? Почему не разделить интерфейс на несколько и, либо реализовывать все и пользоваться всем, либо только часть и не ходить потом по минному полю?
Зачем неизменяемой коллекции методы изменения? Тут чисто идеологический вопрос.
Да, тема не из самых простых :)
Если посмотреть статью википедии на английском, то там упоминается интервью с бабой Варей. Видео есть на ютьюбе. В нем она называет этот принцип behavioral subtyping
Самый простой способ на пальцах объяснить принцип:
Есть класс List. Есть два наследника FirstInFirstOutList и LastInFirstOutList
Следуя принципу, наследники по отношению к пользователю не взаимозаменяемы, потому что их поведение разное.
как то так
Но все же, наследовать квадрат от прямоугольника, или наоборот?
или оба от BaseShape
Все свойства прямоугольника верны для квадрата, но не наоборот. Значит если квадрат унаследовать от прямоугольника, то ППЛ выполняется иначе нет.
На самом деле не все: при увеличении ширины в 2 раза, площадь прямоугольника увеличится в 2 раза, а квадрата — в 4.
Мне кажется здесь проблема в том, что у геометрической фигуры прямоугольник в принципе нет такого метода, как "взять меня и породить новую фигуру со стороной в 2 раза шире". Вы конечно можете создать класс Rectungle с какими угодно методами, но тогда далеко не факт, что все свойства, которые выполняются для геометрической фигуры Прямоугольник продолжат выполняться и для объектов Rectungle с новыми методами.
Квадрат - частный случай прямоугольника и наследовать его не надо ;)
Вот так вот мучаешь себя, запоминаешь, шушукаешься, а потом натыкаешься на это видео и шаблон начинает расходиться по швам:
Это то самое, где вы путаетесь в терминологии и придумываете свои значения для общепринятых терминов?
Честно, очень странное видео. Оно мне напоминает о старой математической шутке, где путем череды избыточных операций и едва едва заметной логической ошибки доказывается, что 2 х 2 = 5. На 3:17 автор зачем-то оборачивает исходные типы в контейнеры, хотя иерархия типов-контейнеров может быть абсолютно произвольной и может никак не соотноситься с исходной иерархией. Ну и далее там тоже имеются вопросики. Почему от этих рассуждений принцип подстановки должен расходиться по швам я так и не понял.
И вообще, я с подозрением отношусь к "революционным" материалам, которые должны объяснить несостоятельность проверенных временем фундаментальных вещей.
В конце концов, я был бы только рад почитать и обсудить статью, которая наглядно и аргументированно разобьет принцип подстановки на глазах у всего сообщества. В ролике я этого не увидел, так что видео, конечно, интересное, но совершенно не возбуждает.
Это иерархия классов может быть произвольной. А соотношение супертип-подтип определяется включением одного множества значений в другое.
Статья вот, можете обсуждать: https://habr.com/ru/articles/521258/
Но автор специально даёт ссылки на другие платформы, где нельзя написать нормальный комментарий к ней.
Спасибо за ссылку! Ознакомился. Обсуждать смысла не вижу, там и без меня прошлись кованым сапогом. И, кто бы мог подумать, революция в информатике в очередной раз откладывается)
Да-да, продолжение банкета не пропустите: https://habr.com/ru/articles/477448/
Пошушукаемся о Барбаре Лисков или раз и навсегда запоминаем принцип подстановки