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

Недавно я начал читать о принципе замещения Лискова (LSP), и я изо всех сил пытаюсь полностью понять последствия ограничения, что «Предварительные условия не могут быть усилены в подтипе». Мне кажется, что это ограничение противоречит принципу проектирования, который предполагает, что следует свести к минимуму или полностью избежать необходимости понижения уровня от базового до производного класса.

То есть я начинаю с класса Animal и вывожу животных Dog, Bird и Human. Ограничение ЛСП на предварительные условия явно соответствует природе, поскольку никакая собака, птица или человек не должны быть ограничены более, чем общий класс животных. Придерживаясь LSP, производные классы будут добавлять специальные функции, такие как Bird.fly() или Human.makeTool(), которые не характерны для Animal.

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

Итак, что мне не хватает?

Бонусный вопрос: еще раз рассмотрим иерархию классов Animals, описанную выше. Ясно, что это было бы нарушением LSP, если бы Animal.setWeight(weight) требовало только неотрицательное число, но Human.setWeight(weight) усилило это предварительное условие и потребовало неотрицательное число меньше 1000. Но как насчет конструктора для Human, который мог бы выглядеть как Human(weight, height, gender)? Будет ли нарушением LSP, если конструктор наложит ограничение на вес? Если да, то как следует изменить эту иерархию, чтобы соблюдать четкие границы физических свойств производных животных?


person Michael Repucci    schedule 22.04.2014    source источник
comment
Обычно код, который хочет работать с Dog, уже имеет его как Dog и (явно или неявно) повышает его до Animal, чтобы передать его коду, который просто хочет работать с точки зрения Animal. Никакого уныния в такой ситуации не происходит.   -  person Damien_The_Unbeliever    schedule 22.04.2014
comment
Значит ли это, что источником плохого дизайна будет попытка кода, который работает с Animals, передать свои Animals коду, который хочет работать только с Dogs?   -  person Michael Repucci    schedule 22.04.2014


Ответы (2)


LSP — это все о поведенческих подтипах. Грубо говоря, B является подтипом A, если его можно всегда использовать там, где ожидается A. Более того, такое использование не должно менять ожидаемого поведения.

Таким образом, при рассмотрении применения LSP главное заключается в том, что такое "ожидаемое поведение A". В вашем примере это Animal. Не так просто разработать полезный Animal интерфейс, общий для всех животных.

Придерживаясь LSP, производные классы будут добавлять специальные функции, такие как Bird.fly() или Human.makeTool(), которые не характерны для Animal.

Не совсем. LSP предполагает, что вы имеете дело только с Animals. Как будто нельзя опускать руки. Итак, ваши Human, Bird и другие животные могут иметь любые методы, конструкторы или что-то еще. Это никак не связано с LSP. Они просто должны вести себя так, как ожидалось, когда используются как Animals.

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

Два распространенных подхода в основных языках ООП:

  1. Понижение
  2. Шаблон посетителя

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

person Stas    schedule 24.04.2014

Многие аспекты программирования требуют компромиссов, и принципы SOLID входят в их число. Если есть какие-то виды действий, которые могут быть выполнены одинаковым образом почти со всеми производными класса или реализациями интерфейса, и на самом деле не являются частью основной цели интерфейса, но некоторые конкретные производные или реализации могут есть лучший способ сделать это, «Принцип разделения интерфейса» предполагает, что такие действия не должны быть включены в общий интерфейс (*). В таких случаях для кода, который получает ссылку на что-то неспецифического типа, может быть полезно проверить, имеет ли фактический объект определенные «специальные» функции, и использовать их, если это так. Например, код, который получает IEnumerable<Animal> и хочет знать, сколько элементов он содержит, может проверить, реализует ли он ICollection<Animal> или необобщенный ICollection [обратите внимание, что List<Cat> реализует последний, но не первый] и, если да, используйте метод Count]. В таких случаях нет ничего плохого в понижении приведения, поскольку метод не требует, чтобы переданные экземпляры реализовывали эти интерфейсы — он просто работает лучше, когда они это делают.

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

Другое использование понижения происходит в случаях, когда у вас будет коллекция групп объектов и известно, что конкретные экземпляры объектов в каждой группе «совместимы» друг с другом, даже если объекты в одной группе могут быть несовместимы с объектами в другой. . Например, метод MaleCat.MateWith() может принимать только экземпляр FemaleCat, а FemaleKangaroo.MateWith() с может принимать только экземпляр MaleKangaroo(), но наиболее практичный способ для Ноя получить коллекцию спаривающихся пар животных будет состоять в том, чтобы каждый тип животных имел метод MateWith(), который принимает Animal и приводит к нужному типу (и, возможно, также имеет свойство CanMateWith()). Если MatingPair создано из FemaleHamster и MaleWolf, попытка вызвать метод Breed() для этой пары завершится ошибкой во время выполнения, но если код избегает создания несовместимых сопряженных пар, такие ошибки никогда не должны возникать. Обратите внимание, что дженерики могут существенно уменьшить потребность в таком преобразовании, но не устранить его полностью.

При определении того, нарушает ли приведение вниз LSP, вопрос на 50 000 долларов заключается в том, будет ли метод поддерживать свой контракт для всего, что может быть передано. возвращает true, тот факт, что при задании некоторых подтипов Animal произойдет сбой, не будет нарушением LSP. В общем, полезно иметь методы, отклоняющие во время компиляции объекты, тип которых не гарантируется для использования, но в некоторых случаях код может иметь сведения о взаимосвязях между типами определенных экземпляров объектов, которые не могут быть выражены синтаксически [например, тот факт, что MatingPair будет содержать два экземпляра Animal, которые могут быть успешно размножены]. Хотя понижение приведения часто является запахом кода, в этом нет ничего плохого, если оно используется способами, согласующимися с контрактом объекта.

person supercat    schedule 22.04.2014