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

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

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

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

Все под защитой

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

Классический аргумент в пользу повсеместного использования модификатора доступа protected по умолчанию выглядит следующим образом: «вы никогда не знаете, какой код вам нужно будет изменить, вы не знаете, как вам нужно будет его изменить, поэтому вы должны используйте модификатор доступа protected, который позволит вам изменить что-либо в вашем коде в будущем'.

Это правда, вы действительно не знаете, что вам нужно расширить, просто вся часть модификатора доступа protected неверна.

Наследование используется как предпочтительный способ расширения функциональности

Использование модификатора доступа protected везде по умолчанию — плохая идея. Аргумент в пользу этого имеет большое допущение, встроенное в него. Предполагается, что наследование является единственным и/или предпочтительным способом расширения функциональности. Это предположение не позволяет вашему коду прожить долгую и здоровую жизнь.

Так в чем проблема с наследованием как способом расширения кода? Звучит красиво в теории. Однако…

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

Чем больше времени проходит, тем сложнее становится делать какие-либо дальнейшие расширения.

Расширение пределов наследования

Допустим, вы решили расширить класс набором protected переменных и методов экземпляра, и предположим, что этот класс отображает HTML-контент на экране.

Вам нужно создать 2 варианта класса: один с функциями отладки, который отображает границы макета, поля и отступы, а другой создает журналы рендеринга с содержимым HTML.

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

Итак, предположим, вы выбрали второй вариант, то есть создать 2 дочерних класса, один с отладкой, а другой с ведением журнала.

Теперь предположим, что у вас есть одно место в коде, которому нужна только функция отладки, и другое место, где нужны функции как отладки, так и регистрации.

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

Вот почему расширение функциональности с помощью наследования становится очень ограничивающим и приводит к безвыходным ситуациям.

Жизнь, вероятно, станет трудной, когда у вас есть 2 или более функций в дочерних классах, потому что это только вопрос времени, когда вам придется использовать комбинации этих функций.

Дополнительные аргументы в пользу чрезмерного использования наследования и защищенных членов

этого не произойдет

Есть еще один аргумент в пользу использования наследования, который звучит так: «да ладно, на самом деле, в большинстве случаев у вас нет нескольких вариантов родительского класса, вы расширяете его только один раз, так что наследование можно использовать”.

У этого аргумента есть изъян, поскольку он предполагает, что вы точно знаете, что вам больше не потребуется расширять функциональность в будущем.

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

Принцип открыто-закрыто

Кроме того, есть еще один аргумент в пользу использования переменных и методов экземпляра protected и использования наследования для расширения функциональности: «если вы создаете переменные экземпляра private, это противоречит принципу открытого-закрытого».

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

Композиция важнее наследования

Использование наследования для расширения функциональности — это временное решение проблем проектирования. Вместо этого композиция должна быть предпочтительным способом расширения функциональности.

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

Повторный пример

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

Средство визуализации HTML может быть сделано таким, чтобы его не заботило, что и как делается для отображения элемента на экране, и он мог бы принимать несколько таких средств визуализации и отображать элементы с предоставленными меньшими классами. Один класс рендеринга может быть реальным рендерером, а другой — отладочным рендерером, вы можете внедрить их как сами по себе, так и в виде композита в класс рендерера HTML и использовать их оба одновременно.

Что касается части логирования, то она действительно не должна быть частью рендерера HTML, но если вы находитесь в ситуации, когда она должна быть его частью, то вы можете сделать то же самое, что и с рендерингом.

Когда вы используете композицию, у вас есть гораздо больше вариантов для реализации чего-либо и гораздо больше свободы для расширения вашей реализации позже.

Отношения

Наследование не имеет большого смысла, когда ваш код хорошо спроектирован, за исключением случаев, когда у вас есть истинное отношение is-a. Отношения is-a в ООП часто определяются следующим образом: "отношение is-a существует, когда есть подтип другого типа". Другими словами, в контексте наследования, когда класс расширяет другой класс.

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

Мне нравится думать об этом немного по-другому. Я думаю о базовых концепциях, которые пытаются представить типы/классы. Если понятия не связаны is-a образом, то не имеет значения, используете вы наследование или нет, это все равно не превращает его в is-a отношение.

Например, если вы расширяете класс рендерера HTML для регистрации каждого рендеринга, это не создает отношения is-a, потому что, если вы посмотрите на концепции, ведение журнала не является рендерингом, даже если в иерархии классов класс ведения журнала расширяет класс рендеринга и семантически это выглядит как отношение is-a.

Вывод заключается в том, что вы должны предпочесть композицию наследованию, когда речь идет о расширении функциональности, потому что наследование очень ограничивает, если вы не имеете дело с реальным отношением is-a.

Загрязненная наследованием кодовая база, которая злоупотребляет модификатором доступа protected, негибкая и должна вызывать беспокойство.

Забудьте классический пример class Dog extends Animal, в котором есть только метод вывода звука, издаваемого животным. Реальный мир сложнее, чем это.

Кроме того, не воспринимайте утверждение «ООП должно моделировать реальный мир» слишком буквально (спорно, насколько верно это утверждение на самом деле (я думаю, что оно немного вводит в заблуждение), но я не буду этого делать). прямо сейчас (только на данный момент)), вы можете чрезмерно использовать наследование.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу