06.08.2022

Принципы ООП. Наследование

Сергей Немчинский
6 минут просмотра
Принципы ООП. Наследование

Наследование – такой интересный принцип, которого большинство современных программистов вообще старается избегать. Что такое наследование? По Википедии это так: «абстрактный тип данных может наследовать данные и функциональность некоторого существующего типа, способствуя повторному использованию компонентов программного обеспечения».

Перевожу на человеческий: один класс может наследовать другой класс, его поля и методы. Что значит наследовать? Переиспользовать. После того, как класс объявляет себя наследником какого-то класса, соответствующие поля и методы появляются в нем автоматически. Этот принцип используется в разных языках. В Java это extenсe, в С++ это двоеточие, в Ruby – треугольная скобочка, и так далее.

Наследование — это форма отношений между классами. Класс-наследник использует методы класса-предка, но не наоборот. Например, класс «собака» является наследником класса «животное», но «животное» не наследует свойства класса «собака». Следовательно, наследник — это более узкий класс сравнительно с предком. 

При этом наследование называется словом extenсe, что значит “расширение”. Например, мы указываем для класса «собака» поле «лапы» — а для класса «животное» мы не можем его использовать, потому что у животных часто вовсе нет лап, если это рыба или змея. Так что класс-наследник может расширять свойства базового класса, используя его код.

Наследование бывает одиночное и множественное. Одиночное – это когда один или несколько классов наследуются только от одного базового класса. Множественное наследование – когда класс наследуется от нескольких базовых классов. Множественное наследование есть во многих языках: вы можете перенести данные и\или поведение из других классов. Но, например, в Java множественное наследование ограничено и возможно только от определенного типа классов, например, от интерфейса – да, интерфейс это тоже класс.

Из-за чего во многих языках ограничивают множественное наследование? Из-за ромбовидного наследования. Когда два класса наследуют свойства одного базового, а затем некий четвертый класс наследует второй и третий одновременно. Получается путаница: непонятно, какая реализация должна использоваться. В некоторых языках, например, Scala, это решили с помощью порядка записи. Но это не такая уж важная проблема: в конце концов, множественное наследование не так уж необходимо, так что не такое большое это и ограничение.

Самое важное. В период моей юности было принято наследовать все от всего и переиспользовать код исключительно через наследование. В результате программисты погрязли в запредельном уровне деревьев наследования. Каждый программист придумывал себе базовый класс (или несколько), от которых наследовалось все. Типичной была ситуация, когда у класса был пятнадцатый или двадцатый уровень наследования. В этих классах могло вообще не быть кода, а названия у них были просто наркоманские. Эта мода привела к тому, что множество ведущих программистов переключилось на делегирование вместо наследование. Это когда класс не наследует, а вызывает другой класс. И они, конечно, были правы, но в результате маятник качнулся в другую сторону.

Сейчас многие начинающие и не очень программисты считают, что наследование не надо использовать никогда, а надо использовать делегирование. Увы, но таким образом они стреляют себе в ногу. Допустим, вы пишете кадровую систему. У вас есть объект типа «инженер», объект типа «бухгалтер», объект типа «менеджер». Если они не являются наследниками от класса «person”, а просто три отдельных класса, то чтобы подсчитать количество сотрудников компании, вам нужно перебрать все три списка. А когда добавится новый вид сотрудников, вам нужно не забыть изменить весь код, который подсчитывает сотрудников, и добавить в него четвертый список. Если же понадобится подсчитать, к примеру, только тех сотрудников, которые находятся в офисе, вы с ума сойдете. У вас пять видов сотрудников, которые между собой не взаимосвязаны. Их обработка займет кучу времени, код вырастает в разы. Это глупо.

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

Да, я понимаю, что со многими фреймворками возникают вопросы, как правильно меппить, как правильно работать с деревьями наследования, что делать, и так далее. Но если вы знакомы с GoF-овскими паттернами, вспомните: почти все они используют наследование и полиформизм. А истинный полиморфизм, как вы догадываетесь, без наследования практически не работает. Поэтому, если вы полностью отказываетесь от наследования, вы отказываетесь от всей мощи ООП и откатываетесь в каменный век, в процедурное программирование. Наследование – это одна из главных сил ООП, и отказываться от нее глупо.