Полиморфизм при наследовании и LSP.



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



Если же родительский класс содержит такой метод, то у нас есть следующие варианты:



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

2. Реализация в дочернем классе полностью сохраняет поведение родительского класса, но делает дополнительную работу или меняет поля, отсутствующие в родительском классе. Мы все также можем использовать дочерний класс там, где ожидается родительский, но в других частях программы мы получаем дополнительные возможности.

3. Мы меняем поведение метода родительского класса, но не нарушаем его важные характеристики. В этом случае мы должны четко понимать, какие требования есть к базовому методу, чтобы не нарушить совместимость. Если мы не соблюдаем принцип инверсии зависимости и базовый класс не является абстрактным, может получиться, что требования к методу слишком конкретные и тогда этот вариант фактически сводится к предыдущему. При этом мы можем расширять область значений параметров метода (снимая некоторые ограничения или переходя к родительским типам), а иногда и сужать область значений результата.

4. Мы сохраняем формальные характеристики метода (сигнатуру, возвращаемое значение), но сильно меняем его поведение. Как правило, это происходит когда требования к методу не выделены или по ошибке. В этом случае инструменты, предоставляемые языком программирования, могут предполагать что методы все ещё совместимы, что не является правдой на самом деле.

5. Мы меняем даже сигнатуру метода несовместимым образом. Например, произвольно меняем тип результата или параметров, но не так как в п.3. Класс однозначно нельзя использовать там, где ожидается родительский и это могут обнаружить автоматические инструменты.



Если мы наследуемся от какого-то объекта, согласно принципу подстановки Барбары Лисков (LSP) мы не должны нарушать совместимость. То есть, если в каком-то коде ожидается экземпляр базового класса, а мы туда подставляем дочерний, код должен работать корректно и согласно нашим ожиданиям.



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



Может показаться, что в точности соблюдая требования, мы лишаемся полиморфизма, однако это не так. Обратите внимание на предложенные выше варианты. (Пример в комментариях)



Таким образом,

• LSP требует совместимости между родительским и дочерними классами на уровне выполнения требований.

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

• Дочерний объект должен проходить тесты, ожидающе экземпляр базового класса

• Использование абстракций позволяет нам добиться большей гибкости при реализации дочерних классов

• Даже при конкретных требованиях у нас есть альтернативные варианты реализации



Дополнительные материалы:

https://news.mit.edu/2009/turing-liskov-0310

https://ru.wikipedia.org/wiki/Абстрактный_тип_данных

https://t.me/advice17/58

https://en.wikipedia.org/wiki/Dependency_inversion_principle