Композиция или наследование: как выбрать? / Хабр
В начале…
… не было ни композиции, ни наследования, только код.
И был код неповоротливым, повторяющимся, нераздельным, несчастным, избыточным и измученным.
Основным инструментом для повторного использования кода была копипаста. Процедуры и функции были редкостью, подозрительными новомодными штучками. Вызов процедур был дорогим удовольствием. Части кода, отделенные от основной логики, вызывали недоумение!
Мрачные были времена.
Но вот лучик ООП воссиял над миром… Правда, несколько десятилетий1 никто этого не замечал. Покуда не появился графический интерфейс2, которому, как выяснилось, очень-очень не хватало ООП. Когда нажимаешь на кнопку в окне, что может быть проще, чем отправить кнопке (или ее представителю) сообщение «Нажатие»3 и получить результат?
И вот тут ООП взлетел. Было написано множество4 книг, расплодились бесчисленные5 статьи. Так что сегодня-то каждый может в объектно-ориентированное программирование, так?
Увы, код (и интернет) говорит, что не так
Самые жаркие споры и наибольшее непонимание, похоже, вызывает выбор между композицией и наследованием, зачастую выраженный мантрой «предпочитайте композицию наследованию». Вот об этом и поговорим.
Когда мантры вредят
В житейском плане «предпочитать композицию наследованию» в целом нормально, хоть я и не любитель мантр. Несмотря на то, что они зачастую и несут зерно истины, слишком легко поддаться соблазну и бездумно следовать лозунгу, не понимая, что за ним скрывается. А это всегда выходит боком.
Желтушные статьи с заголовками вроде «Наследование — зло»6 тоже не по мне, особенно если автор пытается обосновать свои набросы, сначала неправильно применяя наследование, а потом делая вывод, что оно во всем виновато. Ну типа «молотки — отстой, потому что ими нельзя завинтить шуруп.»
Начнем с основ.
Определения
Далее в статье я буду понимать под ООП «классический» объектный язык, который поддерживает классы со свойствами, методами и простое (одиночное) наследование. Никаких вам интерфейсов, примесей, аспектов, множественного наследования, делегатов, замыканий, лямбд, — ничего, кроме самых простых вещей:
- Класс: именованная сущность из предметной области, возможно, имеющая предка (суперкласс), определенная как набор полей и методов.
- Поле: именованное свойство с определенным типом, которое может, в частности, ссылаться на другой объект (см. композиция).
- Метод: именованная функция или процедура, с параметрами или без них, реализующая какое-то поведение класса.
- Наследование: класс может унаследовать — использовать по умолчанию — поля и методы своего предка. Наследование транзитивно: класс может наследоваться от другого класса, который наследуется от третьего, и так далее вплоть до базового класса (обычно —
), возможно, неявного. Наследник может переопределить какие-то методы и поля чтобы изменить поведение по умолчанию. - Композиция: если поле у нас имеет тип Класс, оно может содержать ссылку на другой объект этого класса, создавая таким образом связь между двумя объектами. Не влезая в дебри различий между простой ассоциацией, агрегированием и композицией, давайте «на пальцах» определим: композиция — это когда один объект предоставляет другому свою функциональность частично или полностью.
- Инкапсуляция: мы обращаемся с объектами как с единой сущностью, а не как с набором отдельных полей и методов, тем самым скрываем и защищаем реализацию класса. Если клиентский код не знает ничего, кроме публичного интерфейса, он не может зависеть от деталей реализации.
Наследование фундаментально
Наследование — это фундаментальное понятие ООП. В языке программирования могут быть объекты и сообщения, но без наследования он не будет объектно-ориентированным (только основанным на объектах, но все еще полиморфным).
… как и композиция
Композиция это тоже фундаментальное свойство, причем любого языка. Даже если язык не поддерживает композицию (что редкость в наши дни), люди все равно будут мыслить категориями частей и компонентов. Без композиции было бы невозможно решить сложные задачи по частям.
(Инкапсуляция тоже вещь фундаментальная, но сейчас речь не о ней)
Так от чего весь сыр-бор?
Ну хорошо, и композиция, и наследование фундаментальны, в чем дело-то?
А дело в том, что можно подумать, что одно всегда может заменить другое, или что первое лучше или хуже второго. Разработка ПО — это всегда выбор разумного баланса, компромисс.
С композицией все более-менее просто, мы с ней постоянно сталкиваемся в жизни: у стула есть ножки, стена состоит из кирпичей и цемента и тому подобное. А вот наследование, несмотря на свое простое определение, может все усложнить и запутать, если хорошенько не поразмыслить над тем, как его применять. Наследование это весьма абстрактная штука, о нем можно рассуждать, но так просто его не потрогаешь. Мы, конечно, можем сымитировать наследование, используя композицию, но это, как правило, слишком много возни. Для чего нужна композиция — очевидно: из частей собрать целое. А вот с наследованием сложнее, потому что оно сразу о двух вещах: о смысле и о механике.
Наследование смысловое
Как в биологии классификация таксонов организует их в иерархии, так наследование отражает иерархию понятий из предметной области. Упорядочивает их от общего к частному, собирает родственные идеи в ветви иерархического древа. Смысл (семантика) класса по большей части выражен в его интерфейсе — наборе сообщений, которые класс способен понять, но также определяется и теми сообщениями, которыми класс отвечает. Унаследовался от предка — будь добр не только понять все сообщения, которые мог понять предок, но также и уметь ответить как он (сохранить поведение предка — прим. пер.) И поэтому наследование связывает наследника с предком гораздо сильнее, чем если бы мы взяли просто экземпляр предка как компонент. Обратите внимание, даже если класс делает что-то совсем простое, почти не имеет логики, его имя несет существенную смысловую нагрузку, разработчик делает из него важные выводы о предметной области.
Наследование механическое
Говоря о наследовании в механическом плане, мы имеем в виду, что наследование берет данные (поля) и поведение (методы) базового класса и позволяет использовать их повторно или же дополнить в наследниках. С точки зрения механики, если потомок унаследует реализацию (код) предка, то неизбежно получит и его интерфейс.
Я уверен, что в недопонимании виновата именно эта двойственная природа наследования7 в большинстве ОО-языков. Многие считают, что наследование — это чтобы повторно использовать код, хотя оно не только для этого. Если придавать повторному использованию чрезмерное значение — жди беды в архитектуре. Вот пара примеров.
Как не надо наследовать. Пример 1
class Stack extends ArrayList { public void push(Object value) { … } public Object pop() { … } }
Казалось бы, класс Stack
, все хорошо. Но посмотрите внимательно на его интерфейс. Что должно быть в классе с именем Stack? Методы push()
и pop()
, что же еще. А у нас? У нас есть get()
, set()
, add()
, remove()
, clear()
и еще куча барахла, доставшегося от ArrayList
, которое стеку ну вообще не нужно.
Можно было бы переопределить все нежелательные методы, а некоторые (например, clear()
) даже и адаптировать под наши нужды, но не многовато ли работы из-за одной ошибки в дизайне? На самом деле трех: одной смысловой, одной механической и одной комбинированной:
- Утверждение «Stack это ArrayList» ложно.
Stack
не является подтипомArrayList
. Задача стека — обеспечить выполнение правила LIFO (последним пришел, первым ушел), которое легко удовлетворяется интерфейсом push/pop, но никак не соблюдается интерфейсомArrayList
. - Механически наследование от
ArrayList
нарушает инкапсуляцию. Клиентскому коду не должно быть известно, что мы решили использоватьArrayList
для хранения элементов стека. - Ну и наконец, реализуя стек через
ArrayList
мы смешиваем две разные предметные области:ArrayList
— это коллекция с произвольным доступом, а стек — это понятие из мира очередей, со строго ограниченным (а не произвольным)8 доступом.
Последний пункт — незначительная на первый взгляд, но важная вещь. Посмотрим на нее пристальнее.
Как не надо наследовать. Пример 2
Частая ошибка при наследовании — это создать модель из предметной области, унаследовав ее от готовой реализации. Вот, скажем, нам надо выделить некоторых наших клиентов (класс Customer
) в определенное подмножество. Легко! Наследуемся от ArrayList<Customer>
, называем это CustomerGroup
и понеслась.
Не тут-то было. Поступив так мы опять спутаем две предметные области. Старайтесь избегать этого:
ArrayList<Customer>
это уже наследник списка, утилиты типа «коллекция», готовой реализации.CustomerGroup
это совсем другая штука — класс из предметной области (домена).- Классы из предметной области должны использовать реализации, а не наследовать их.
Слой предметной области не должен знать, как у нас там все внутри сделано. Рассуждая о том, что делает наша программа, мы оперируем понятиями из предметной области, и мы не хотим отвлекаться на нюансы внутреннего устройства. Если видеть в наследовании только инструмент повторного использования кода, мы раз за разом будем попадаться в эту ловушку.
Дело не в одиночном наследовании
Одиночное наследование пока остается самой популярной моделью ООП. Оно неизбежно влечет наследование реализации, которое приводит к сильному зацеплению (coupling — прим. пер.) между классами. Может показаться, что беда в том, что ветка наследования у нас только одна на обе потребности: и смысловую и механическую. Если использовали для одного, то для другого уже нельзя. А раз так, может быть множественное наследование все исправит?
Нет. Отношение наследования не должно пересекать границы между предметными областями: инструментальной (структуры данных, алгоритмы, сети) и прикладной (бизнес-логика). Если CustomerGroup будет наследовать ArrayList<Customer>
и одновременно, скажем, DemographicSegment, то две предметные области переплетутся между собой, а «видовая принадлежность» объектов станет неочевидна.
Предпочтительно (по крайней мере, с моей точки зрения) делать так. Наследуемся от имеющихся в языке инструментальных классов по минимуму, ровно настолько, чтобы реализовать «механическую» часть вашей логики. Потом соединяем получившиеся части композицией, но не наследованием. Иными словами:
От инструментов можно наследовать только другие инструменты.
Это очень частая ошибка новичков. Что не удивительно, ведь так просто взять и унаследовать. Редко где встретишь обсуждения, почему именно это неправильно. Еще раз: бизнес-сущности должны пользоваться инструментами, а не быть ими. Мухи (инструменты) — отдельно, котлеты (бизнес-модели) — отдельно.
Так когда же нужно наследование?
Наследуемся как надо
Чаще всего — и при этом с наибольшей отдачей — наследование применяют для описания объектов, незначительно отличающихся друг от друга (в оригинале используется термин «differential programming» — прим. пер.) Например, нам нужна особенная кнопка с небольшими дополнениями. Нормально, наследуемся от существующего класса Кнопка. Потому что наш новый класс, это все еще кнопка, а мы полностью наследуем API класса Кнопка, его поведение и реализацию. Новая функциональность только добавляется к существующему. А вот если в наследнике часть функциональности убирается, это повод задуматься, а нужно ли наследование.
Наследование полезнее всего для группировки сходных сущностей и понятий, определения семейств классов, и вообще для организации терминов и понятий, описывающих предметную область. Зачастую, когда значительная часть предметной логики уже реализована, исходно выбранные иерархии наследования перестают работать. Если всё к тому идет, не бойтесь разобрать и заново сложить эти иерархии9 так, чтобы они лучше соответствовали и работали друг с другом.
Композиция или наследование: что выбрать?
В ситуации, когда вроде бы подходит и то и другое, взгляните на дизайн в двух плоскостях:
- Структура и механическое исполнение бизнес-объектов.
- Что они обозначают по смыслу и как взаимодействуют.
Пока наследование остается внутри одной плоскости, все нормально. Но если иерархия проходит через две плоскости сразу, это плохой симптом.
Например, у вас есть один объект внутри другого. Внутренний объект реализует значительную часть поведения внешнего. У внешнего объекта куча прокси-методов, которые тупо пробрасывают параметры во внутренний объект и возвращают от него результат. В этом случае посмотрите, а не стоит ли унаследоваться от внутреннего объекта, хотя бы частично.
Разумеется, никакие инструкции не заменят голову на плечах. Когда строишь объектную модель, вообще полезно думать. Но если вам хочется конкретных правил, то пожалуйста.
Наследуем, если:
- Оба класса из одной предметной области
- Наследник является корректным подтипом (в терминах LSP — прим. пер.) предка
- Код предка необходим либо хорошо подходит для наследника
- Наследник в основном добавляет логику
Иногда все эти условия выполняются одновременно:
- в случае моделирования высокоуровневой логики из предметной области
- при разработке библиотек и расширений для них
- при дифференциальном программировании (автор снова использует термин «differential programming», очевидно, понимая под ним нечто, отличное от DDP — прим. пер.)
Если это не ваш случай, то и наследование вам, скорее всего, будет нужно не часто. Но не потому, что надо «предпочитать» композицию наследованию, и не потому что она «лучше». Выбирайте то, что подходит наилучшим образом для конкретно вашей задачи.
Надеюсь, эти правила помогут вам понять разницу между двумя подходами.
Приятного кодинга!
Послесловие
Отдельная благодарность сотрудникам ThoughtWorks за их ценный вклад и замечания: Питу Хогсону, Тиму Брауну, Скотту Робинсону, Мартину Фаулеру, Минди Ор, Шону Ньюхэму, Сэму Гибсону и Махендре Кария.
1
Первый официальный ОО-язык, SIMULA 67, появился в 1967 году.
2
Системные и прикладные программисты приняли на вооружение C++ в середине 1980-х, но перед тем, как ООП стал общепринятым, прошел еще десяток лет.
3
Я намеренно упрощаю, не говорю про паб/саб, делегатов и тому подобное, чтобы не раздувать статью.
4
На момент написание этого текста Амазон предлагает 24777 книг по ООП.
5
Поиск в гугле по фразе «объектно-ориентированное программирование» дает 8 млн результатов.
6
Поиск в гугле выдает 37600 результатов по запросу «наследование это зло».
7
Смысл (интерфейс) и механику (исполнение) можно разделить за счет усложнения языка. См. пример из спецификации языка D.
8
С грустью замечу, что в Java Stack
унаследован от Vector
.
9
Проектирование для повторного использования через наследования выходит за рамки темы статьи. Просто имейте в виду, что ваш дизайн должен удовлетворить потребности и тех, кто пользуется базовым классом, и тех, кому нужен наследник.
Переводчик выражает благодарность ООП-чату в Telegram, без которого этот текст не смог бы появиться.
Принципы ООП. Наследование — FoxmindEd
Наследование – такой интересный принцип, которого большинство современных программистов вообще старается избегать. Что такое наследование? По Википедии это так: «абстрактный тип данных может наследовать данные и функциональность некоторого существующего типа, способствуя повторному использованию компонентов программного обеспечения».
Перевожу на человеческий: один класс может наследовать другой класс, его поля и методы. Что значит наследовать? Переиспользовать. После того, как класс объявляет себя наследником какого-то класса, соответствующие поля и методы появляются в нем автоматически. Этот принцип используется в разных языках. В Java это extenсe, в С++ это двоеточие, в Ruby – треугольная скобочка, и так далее.
Наследование — это форма отношений между классами. Класс-наследник использует методы класса-предка, но не наоборот. Например, класс «собака» является наследником класса «животное», но «животное» не наследует свойства класса «собака». Следовательно, наследник — это более узкий класс сравнительно с предком.
При этом наследование называется словом extenсe, что значит “расширение”. Например, мы указываем для класса «собака» поле «лапы» — а для класса «животное» мы не можем его использовать, потому что у животных часто вовсе нет лап, если это рыба или змея. Так что класс-наследник может расширять свойства базового класса, используя его код.
Наследование бывает одиночное и множественное. Одиночное – это когда один или несколько классов наследуются только от одного базового класса. Множественное наследование – когда класс наследуется от нескольких базовых классов. Множественное наследование есть во многих языках: вы можете перенести данные и\или поведение из других классов. Но, например, в Java множественное наследование ограничено и возможно только от определенного типа классов, например, от интерфейса – да, интерфейс это тоже класс.
Из-за чего во многих языках ограничивают множественное наследование? Из-за ромбовидного наследования. Когда два класса наследуют свойства одного базового, а затем некий четвертый класс наследует второй и третий одновременно. Получается путаница: непонятно, какая реализация должна использоваться. В некоторых языках, например, Scala, это решили с помощью порядка записи. Но это не такая уж важная проблема: в конце концов, множественное наследование не так уж необходимо, так что не такое большое это и ограничение.
Самое важное. В период моей юности было принято наследовать все от всего и переиспользовать код исключительно через наследование. В результате программисты погрязли в запредельном уровне деревьев наследования. Каждый программист придумывал себе базовый класс (или несколько), от которых наследовалось все. Типичной была ситуация, когда у класса был пятнадцатый или двадцатый уровень наследования. В этих классах могло вообще не быть кода, а названия у них были просто наркоманские. Эта мода привела к тому, что множество ведущих программистов переключилось на делегирование вместо наследование. Это когда класс не наследует, а вызывает другой класс. И они, конечно, были правы, но в результате маятник качнулся в другую сторону.
Сейчас многие начинающие и не очень программисты считают, что наследование не надо использовать никогда, а надо использовать делегирование. Увы, но таким образом они стреляют себе в ногу. Допустим, вы пишете кадровую систему. У вас есть объект типа «инженер», объект типа «бухгалтер», объект типа «менеджер». Если они не являются наследниками от класса «person”, а просто три отдельных класса, то чтобы подсчитать количество сотрудников компании, вам нужно перебрать все три списка. А когда добавится новый вид сотрудников, вам нужно не забыть изменить весь код, который подсчитывает сотрудников, и добавить в него четвертый список. Если же понадобится подсчитать, к примеру, только тех сотрудников, которые находятся в офисе, вы с ума сойдете. У вас пять видов сотрудников, которые между собой не взаимосвязаны. Их обработка займет кучу времени, код вырастает в разы. Это глупо.
Я видел, как программисты отказывались делать наследование там, где оно буквально напрашивалось. Мой личный принцип, который я и вам советую: используйте наследование, когда эти объекты действительно проистекают друг из друга. Глупо наследовать несвязанный объект просто для того, чтобы унаследовать свойства: тут лучше применить делегирование. Но в очевидных случаях, отказавшись от наследования, вы выстрелите себе в ногу и создадите массу проблем на ровном месте.
Да, я понимаю, что со многими фреймворками возникают вопросы, как правильно меппить, как правильно работать с деревьями наследования, что делать, и так далее. Но если вы знакомы с GoF-овскими паттернами, вспомните: почти все они используют наследование и полиформизм. А истинный полиморфизм, как вы догадываетесь, без наследования практически не работает. Поэтому, если вы полностью отказываетесь от наследования, вы отказываетесь от всей мощи ООП и откатываетесь в каменный век, в процедурное программирование. Наследование – это одна из главных сил ООП, и отказываться от нее глупо.
Наследование в программировании: лучшие практики и допустимые варианты использования
Наследование — это мощная концепция в программировании, которая позволяет классам наследовать свойства и поведение от родительских классов. При правильном использовании он может улучшить читаемость кода, уменьшить избыточность и упростить разработку программного обеспечения. Однако при неправильном использовании наследование может привести к тому, что код будет труден для понимания, сопровождения и изменения.
К сожалению, в программировании наследование часто используется неправильно. Разработчики полагаются на него как на универсальное решение, не принимая во внимание его потенциальные недостатки. Это может привести к раздутому или негибкому коду, с которым со временем будет трудно работать. Поэтому разработчикам важно понимать, когда и как правильно использовать наследование. Для того, чтобы писать код, который эффективен, удобен в сопровождении и с которым легко работать.
Ранее мы говорили о передовых практиках по другим темам, например, о передовых практиках в отношении исключений .Net. В этой статье мы рассмотрим допустимые и недопустимые варианты использования наследования в программировании и предложим рекомендации по максимальному использованию этой мощной концепции.
Недостатки наследования в программировании
Хотя наследование может быть полезным инструментом в программировании, в некоторых ситуациях это может быть плохой практикой. Здесь я перечислю причины, по которым в некоторых ситуациях следует избегать наследования.
Герметичная муфта
Наследование может создать тесную связь между классами, что означает, что изменения в одном классе могут иметь неожиданные последствия для других. Тесная связь возникает, когда два или более класса сильно зависят друг от друга, а это означает, что изменения в одном классе могут повлиять на функциональность другого. Это может привести к тому, что код будет сложно модифицировать, поддерживать и понимать с течением времени.
Одна из причин, по которой наследование может привести к тесной связи, заключается в том, что оно создает иерархические отношения между классами. Подклассы наследуют свойства и поведение от родительских классов, и изменения в родительском классе могут повлиять на поведение подкласса. Если несколько подклассов наследуются от одного и того же родительского класса, изменения в этом родительском классе могут иметь волновой эффект во всей кодовой базе.
Еще одна причина, по которой наследование может создать тесную связь, заключается в том, что оно побуждает разработчиков писать код, который сильно зависит друг от друга. Например, подкласс может в значительной степени полагаться на поведение своего родительского класса, что затрудняет изменение подкласса без изменения родительского класса.
Тесная связь может затруднить поддержку и изменение кода с течением времени. Если изменения в одном классе оказывают неожиданное влияние на другие, может быть сложно отследить ошибки или понять, как работает код. Это может привести к тому, что с кодовой базой будет сложно работать, и со временем это может замедлить разработку.
Чтобы избежать тесной связи при использовании наследования, важно тщательно рассмотреть отношения между классами и потенциальное влияние изменений в родительском классе на его подклассы. Кроме того, другие методы программирования, такие как композиция или внедрение зависимостей, могут использоваться для уменьшения связанности и создания более модульного кода.
Повторное использование кода
Хотя наследование может быть полезным для повторного использования кода, оно также может привести к проблемам с повторным использованием кода, если использовать его неаккуратно. Наследование способствует повторному использованию кода, позволяя подклассам наследовать свойства и поведение родительских классов, уменьшая необходимость дублирования кода. Однако это также может создать зависимости, затрудняющие повторное использование кода в разных контекстах.
Одним из способов, которым наследование может привести к проблемам повторного использования кода, является поощрение разработчиков к созданию больших монолитных классов, тесно связанных с другими классами. Это может затруднить повторное использование кода в разных контекстах, поскольку код сильно зависит от других классов и может быть недостаточно модульным или гибким для использования в других контекстах.
Кроме того, наследование может привести к проблемам с повторным использованием кода из-за продвижения универсального подхода к разработке программного обеспечения. Подклассы наследуют свойства и поведение от родительских классов, что может создать жесткую структуру кода, которую трудно изменить или расширить. Это может привести к ситуациям, когда разработчикам придется создавать новые подклассы или модифицировать существующие, чтобы добавить функциональность, даже если эта функциональность будет полезна в других частях кодовой базы.
Чтобы решить эти проблемы повторного использования кода, разработчикам следует рассмотреть другие методы программирования, такие как композиция или внедрение зависимостей. Эти методы позволяют создавать более модульные и гибкие структуры кода, упрощая повторное использование кода в различных контекстах. Кроме того, разработчики должны помнить о зависимостях между классами при использовании наследования и стремиться создавать как можно более модульный и гибкий код. Выполняя эти шаги, разработчики могут избежать проблем с повторным использованием кода при использовании наследования в своих кодовых базах.
Негибкость
Наследование может привести к негибкости в программировании из-за его иерархической природы, которая может создать жесткую структуру кода, которую трудно изменить или расширить. Наследование устанавливает отношения родитель-потомок между классами, где подклассы наследуют свойства и поведение своих родительских классов. Хотя это может привести к тому, что код будет легче читать и поддерживать, это также может привести к тому, что кодовая база станет негибкой и ее будет трудно модифицировать с течением времени.
Одним из способов, которым наследование может привести к негибкости, является создание глубокой иерархии классов. Поскольку подклассы наследуют свойства и поведение своих родительских классов, иерархия со временем может стать глубже и сложнее. Это может затруднить модификацию или расширение кодовой базы, поскольку изменения в одном классе могут иметь волновой эффект во всей иерархии.
Кроме того, наследование может привести к негибкости, продвигая «фиксированную» структуру кода, которую трудно изменить или расширить. Наследование устанавливает между классами жесткие родительско-дочерние отношения, что может затруднить изменение поведения одного класса без воздействия на другие классы в иерархии. Это может затруднить добавление новых функций в кодовую базу или изменение существующих функций без изменения других частей кода.
Чтобы избежать негибкости при использовании наследования, важно тщательно рассмотреть отношения между классами и потенциальное влияние изменений в родительском классе на его подклассы. Кроме того, другие методы программирования, такие как композиция или внедрение зависимостей, могут использоваться для создания более гибких структур кода, которые легче модифицировать и расширять с течением времени. Используя эти методы и помня о потенциальных недостатках наследования, разработчики могут создавать код, который будет удобен для чтения и сопровождения, а также будет достаточно гибким, чтобы адаптироваться к изменяющимся требованиям с течением времени.
Множественное наследование
Множественное наследование — это метод программирования, при котором подкласс наследуется от двух или более родительских классов. Хотя это может быть полезно в некоторых случаях, это также может привести к ряду проблем, которые делают его плохой идеей во многих ситуациях.
Одним из основных недостатков множественного наследования является то, что оно может создавать неоднозначность в кодовой базе. Когда подкласс наследуется от двух или более родительских классов, могут возникать конфликты между методами и свойствами родительских классов. Это может затруднить определение того, какой метод или свойство следует использовать в данной ситуации, и может привести к ошибкам в коде.
Другая проблема с множественным наследованием заключается в том, что оно может привести к созданию сложной и трудной для понимания кодовой базы. По мере добавления большего количества родительских классов отношения между классами становятся более сложными, что затрудняет понимание того, как изменения в одном классе повлияют на остальную часть кода. Это может затруднить поддержку кода с течением времени и может привести к ошибкам и ошибкам.
Наконец, множественное наследование также может затруднить повторное использование кода в других контекстах. Поскольку множественное наследование — это особый метод, основанный на определенной структуре, может быть сложно повторно использовать код, разработанный с использованием множественного наследования, в другом контексте. Это может привести к проблемам повторного использования кода и усложнить поддержку и расширение кодовой базы с течением времени.
В целом, хотя в некоторых случаях множественное наследование может быть полезным методом, в современном программировании оно обычно считается плохой идеей. Чтобы избежать проблем, связанных с множественным наследованием, разработчикам следует рассмотреть другие методы программирования, такие как композиция или внедрение зависимостей, которые могут предложить многие из тех же преимуществ, но без недостатков множественного наследования.
Малоизвестные проблемы с наследованием в программировании
Хотя наследование является мощной и широко используемой техникой программирования, есть несколько менее распространенных случаев, когда оно может вызывать проблемы. Вот несколько примеров:
Круговое наследование
Циклическое наследование происходит, когда два или более класса наследуются друг от друга по циклическому шаблону. Это может создать ряд проблем, таких как бесконечные циклы, дублирование кода и проблемы с иерархиями наследования.
Бриллиантовое наследство
Алмазное наследование происходит, когда подкласс наследуется от двух или более классов, имеющих общий родительский класс. Это может создать неоднозначность в кодовой базе, поскольку могут быть унаследованы несколько версий одного и того же метода или свойства.
Проблема хрупкого базового класса
Проблема хрупкого базового класса возникает, когда изменение в базовом классе может иметь непредвиденные последствия для подклассов, которые от него унаследованы. Это может создать ситуацию, когда, казалось бы, безобидное изменение может вызвать широко распространенные проблемы во всей кодовой базе.
Нарушение принципа замены Лисков
Принцип замещения Лисков гласит, что подклассы должны иметь возможность заменять свои родительские классы без ущерба для корректности программы. Нарушения этого принципа могут привести к неожиданному поведению в кодовой базе.
Герметичная муфта
Хотя тесная связь является распространенной проблемой наследования, она может проявляться менее распространенными способами. Например, подклассы могут наследовать свойства или методы, которые им не нужны, что приводит к ненужной сложности и потенциальным ошибкам.
Допустимые варианты использования наследования в программировании
Да, у использования наследования в программировании есть недостатки, но давайте не будем забывать, что наследование — это основная концепция объектно-ориентированного программирования (ООП). Это означает, что есть много сценариев, в которых мы можем использовать это в наших интересах. Но мы должны быть осторожны, чтобы делать это только тогда, когда для этого есть действительные варианты использования. В следующих разделах я собираюсь перечислить варианты использования, которые действительно допустимы при использовании наследования.
Повторное использование кода
Наследование — это мощный механизм повторного использования кода, который позволяет создавать новые классы на основе существующих, называемых родительскими или базовыми классами. Производный или дочерний класс наследует все атрибуты и методы своего родительского класса, а также может добавлять свои собственные атрибуты и методы или переопределять унаследованные. Вот несколько способов, которыми наследование может улучшить повторное использование кода.
- Избегайте дублирования: Наследование позволяет избежать дублирования кода, определяя общие атрибуты и методы в базовом классе, а затем наследуя их в производных классах. Это упрощает повторное использование кода, поскольку производные классы автоматически имеют все атрибуты и методы, определенные в базовом классе.
- Поддерживайте согласованность: определяя базовый класс с набором общих атрибутов и методов, вы можете обеспечить согласованность всех производных классов. Это упрощает поддержку кода и позволяет избежать ошибок, которые могут возникнуть при использовании разных реализаций одной и той же функциональности.
- Повышение модульности: Наследование повышает модульность, позволяя создавать иерархию связанных классов, где каждый класс наследуется от своего родителя. Это создает четкую взаимосвязь между классами, облегчая понимание кода и его модификацию при необходимости.
- Поощрять инкапсуляцию: Наследование поощряет инкапсуляцию, отделяя детали реализации от интерфейса. Базовый класс может определять интерфейс, а производные классы могут реализовывать детали. Это упрощает изменение реализации, не затрагивая интерфейс, что способствует гибкости и повторному использованию.
- Сокращение времени разработки: Наследование может сократить время разработки, предоставляя структуру повторно используемого кода, который можно легко расширять или изменять. Это может сэкономить время и усилия, поскольку разработчики могут сосредоточиться на создании новых функций поверх существующего кода, а не начинать с нуля.
Полиморфизм
Полиморфизм является важной концепцией разработки программного обеспечения, поскольку он позволяет писать код, который является более гибким, модульным и пригодным для повторного использования. Это позволяет обрабатывать объекты разных классов так, как если бы они были одного класса, а это означает, что можно написать код, который работает с объектами разных типов без необходимости сложных условных операторов или проверки типов.
Наследование — ключевой фактор полиморфизма. Производные классы можно рассматривать как объекты своего родительского класса, что позволяет писать код, работающий с объектами разных классов единообразно. Это важно при разработке программного обеспечения, где разработчикам часто приходится работать с объектами разных типов. Это, в свою очередь, приводит нас к следующим характеристикам, которые необходимы для написания хорошего программного обеспечения.
- Повторное использование кода: позволяет разработчикам писать код, который можно повторно использовать в нескольких проектах и в разных контекстах. Рассматривая объекты разных классов так, как если бы они принадлежали к одному классу, разработчики могут писать код, работающий с различными объектами, без необходимости писать специализированный код для каждого объекта.
- Гибкость: делает код более гибким, позволяя разработчикам легко добавлять новые типы объектов в свой код без необходимости изменять существующий код. Это особенно важно в больших программных системах, где требования могут часто меняться.
- Модульность: повышает модульность, позволяя разработчикам создавать иерархию связанных классов с общим интерфейсом. Этот интерфейс можно использовать для написания кода, который работает с любым объектом, реализующим интерфейс, что упрощает поддержку и модификацию кода с течением времени.
- Инкапсуляция: Способствует инкапсуляции, отделяя детали реализации от интерфейса. Это означает, что разработчики могут изменять реализацию класса, не затрагивая код, работающий с классом.
- Более чистый код: может привести к более чистому коду за счет уменьшения количества условных операторов и проверок типов, необходимых в коде. Это облегчает чтение, понимание и поддержку кода с течением времени.
Упрощение обслуживания кода
Допустим, у нас есть система программного обеспечения, которая включает в себя различные типы транспортных средств, таких как автомобили, грузовики и мотоциклы. Каждый тип транспортного средства имеет некоторые общие атрибуты, такие как модель, год выпуска и цвет, а также некоторые уникальные атрибуты и поведение. Мы можем использовать наследование для создания базового класса с именем 9.0121 Транспортное средство , которое определяет общие атрибуты и поведение, а затем создает производные классы для каждого типа транспортного средства, которые наследуют от базового класса и добавляют свои собственные уникальные атрибуты и поведение.
Используя наследование для создания класса Bicycle
, мы смогли повторно использовать общие атрибуты и поведения, определенные в классе Vehicle
, и добавить только уникальные атрибуты и поведения, характерные для велосипедов. Это не только экономит время на написание нового кода, но и упрощает обслуживание системы с течением времени. Например, если нам нужно изменить start
для всех транспортных средств в системе, мы можем внести изменение в класс Vehicle
, и оно будет автоматически унаследовано всеми производными классами, включая Bicycle
. Если нам нужно изменить метод display_info
только для велосипедов, мы можем внести изменения в класс Bicycle
, не затрагивая код, который работает с другими типами транспортных средств.
Улучшение организации кода
Допустим, у нас есть программа, в которой задействованы несколько типов сотрудников, например штатные сотрудники, сотрудники, работающие неполный рабочий день, и подрядчики. Каждый тип сотрудников имеет некоторые общие атрибуты, такие как имя и идентификационный номер, а также некоторые уникальные атрибуты и поведение. Мы можем использовать наследование для создания базового класса с именем 9.0121 Сотрудник , который определяет общие атрибуты и модели поведения, а затем создает производные классы для каждого типа сотрудников, которые наследуются от базового класса и добавляют свои собственные уникальные атрибуты и модели поведения.
Теперь предположим, что мы хотим добавить в нашу программу новый тип сотрудников, например, стажеров. Вместо того, чтобы создавать новый класс с нуля, мы можем создать новый производный класс с именем Стажер
, который наследуется от класса Сотрудник
и добавляет свои собственные уникальные атрибуты и поведение. Вот пример реализации Стажер
класс:
Используя наследование для создания класса Стажер
, мы смогли повторно использовать общие атрибуты и модели поведения, определенные в классе Сотрудник
, и добавить только уникальные атрибуты и модели поведения, характерные для стажеров. Это делает программу более организованной и простой в обслуживании с течением времени, поскольку каждый тип сотрудников имеет свой собственный класс и может быть изменен отдельно, не затрагивая код, который работает с другими типами сотрудников.
Специализация
Специализация в наследовании относится к процессу создания подкласса, более специфичного, чем суперкласс, путем добавления дополнительных свойств и методов или изменения существующих.
Другими словами, специализация наследования позволяет создать новый класс, являющийся специализированной версией существующего класса, с некоторыми дополнительными или измененными функциями. Подкласс наследует все свойства и методы суперкласса, но также может иметь свои уникальные особенности. Это позволяет повторно использовать код и избегать избыточности за счет создания иерархии связанных классов. Суперкласс предоставляет общий набор функций, в то время как подкласс предоставляет более конкретные функции, адаптированные к конкретному варианту использования.
Хорошим примером использования специализации в наследовании в C# является создание иерархии классов для представления различных типов транспортных средств.
Например, вы можете начать с базового класса «Транспортное средство», который имеет свойства и методы, общие для всех транспортных средств, такие как «Марка», «Модель», «Год», «Объем двигателя», «Тип топлива», и т. д.
Далее вы можете создавать специализированные классы, которые наследуются от класса «Автомобиль» и добавлять свои собственные уникальные свойства и методы. Например, вы можете создать класс с именем «Автомобиль», который наследуется от «Транспортное средство» и добавляет такие свойства, как «Количество дверей», «Тип трансмиссии», «Тип привода» и т. д. Вы также можете создать класс с именем «Грузовик», который наследуется от «Транспортное средство» и добавляет такие свойства, как «Полезная нагрузка», «Токинг-мощность», «TruckBedLength» и т. д.
Вот пример кода:
В этом примере класс «Автомобиль» предоставляет общий набор свойств и методов для всех типов транспортных средств, а классы «Автомобиль» и «Грузовик» наследуют эти свойства и методы и добавляют свои собственные уникальные черты. Это позволяет создавать специализированные классы, предназначенные для конкретных типов транспортных средств, при этом повторно используя код и избегая избыточности.
Резюме
В целом, хотя наследование может быть полезным инструментом в определенных ситуациях, его следует использовать осторожно и с учетом потенциальных недостатков. Другие методы программирования, такие как композиция или внедрение зависимостей, могут быть более подходящими в некоторых ситуациях.
Поделиться…
Состав против наследования
Состав против наследования относится к двум основным понятиям в объектно-ориентированном программировании (ООП). Они являются строительными блоками ООП, и оба работают над моделированием отношений между двумя классами, чтобы разработчикам было проще писать повторно используемый код.
Композиция и наследование относятся к тому, как каждая из этих концепций может помочь программистам повторно использовать код. Повторно используемый код — это использование существующего программного обеспечения для создания нового программного обеспечения с соблюдением принципов повторного использования.
Повторное использование кода — это основная продуктивная функциональность в ИТ, которая достигается путем установления отношений между классами объектов посредством композиции и наследования.
Разница между наследованием и композицией заключается в том, что классы и объекты в коде наследования тесно связаны, а это означает, что их не следует изменять, поскольку изменение родительского или суперкласса может привести к изменению подкласса или дочернего класса, что нарушит ваш код.
С другой стороны, в композиции классы и объекты слабо связаны, что означает, что вы можете легко переключать эти компоненты, не нарушая код.
Это делает композицию более гибкой, поэтому многие разработчики считают, что это лучше наследования, но это чрезмерное упрощение.
Давайте подробнее рассмотрим, как работают эти две концепции, а также преимущества использования композиции по сравнению с наследованием.
Что такое наследование?
Наследование, наряду с абстракцией, инкапсуляцией и полиморфизмом, является одной из четырех основных концепций объектно-ориентированного программирования.
Наследование означает, что класс может наследовать поля и методы своего суперкласса.
Другими словами, новый класс может использовать по умолчанию поля и методы из абстрактного класса, который наследуется от другого класса и т. д., вплоть до различных классов до базового класса.
Иерархию наследования можно сравнить с семейной линией. Ребенок наследует характеристики и поведение одного или нескольких объектов. Производный класс обычно имеет отношение «является» к базовому классу.
Пример наследования;
- Samsung — это мобильное устройство.
- Samsung Galaxy S22 является наследством Samsung.
Отношения наследования — это способ повторного использования кода, который уже был написан на языке программирования Java. В этих примерах Samsung является подклассом базового класса Mobile.
Вы можете наследовать от Mobile, чтобы создать класс Samsung, и вы можете наследовать от Samsung, чтобы создать класс Samsung S22. Итак, интерфейс и реализация базового класса Mobile наследуются подклассом Samsung.
Этот пример известен как гибридное наследование, и здесь представлены другие типы наследования;
Преимущества наследования:
- Облегчает повторное использование кода из родительского класса без необходимости его копирования
- Обеспечивает четкую иерархическую структуру, позволяющую разбить модель на простые и доступные компоненты.
Что такое композиция?
Композиция — это альтернатива наследованию классов, которая служит другим целям. По композиции модели имеют отношение «имеет отношения».
Позволяет создавать сложные типы, комбинируя поведение и характеристики других типов.
В классе, созданном с помощью композиции, есть составная сторона, представляющая собой набор экземпляров компонентов, и составная сторона, представляющая собой экземпляры, которые будет содержать составной класс.
Композиция возникает, когда объект содержит другой объект, и содержащийся объект не может существовать без существования другого объекта.
Кто-то может спросить, что такое композиция в Java?
Ответ будет;
Композиция — это способ разработки Java для реализации отношения «имеет-а». Композиция достигается за счет использования переменной экземпляра, которая ссылается на другие объекты.
Итак, композиция — это процесс объединения многочисленных компонентов для создания нового результата. Композицию можно определить как экземпляр, который обеспечивает некоторые или все свои функции за счет использования другого объекта.
Вот пример;
Представьте, что мы создаем класс под названием «Редкая фотография», который содержит информацию о создателях изображений, такую как автор, дата создания и название.
Теперь мы создадим новый класс под названием Альбом, который имеет ссылку на список фотографий. В альбом могут быть включены многочисленные фотографии из одной или разных категорий.
Таким образом, если альбом будет уничтожен, все фотографии в этом конкретном альбоме будут уничтожены. Без альбома изображения в этом сценарии невозможны.
Композиция альбома и фотографии определяют их взаимосвязь.
Преимущества композиции
- Меньше зависимостей, чем наследование
- Объекты определяются при запуске, поэтому они не могут получить доступ к закрытым данным другого объекта.
Сравнение между ними;
Наследование :
- Один объект приобретает характеристики одного или нескольких других объектов
- Наследование классов определяется во время выполнения
- Предоставляет как общедоступные, так и защищенные базовые классы
- Нет контроля доступа
- Часто нарушает инкапсуляцию
Состав:
- Использование объекта внутри другого объекта.
- Определяется динамически во время выполнения.
- Внутренние детали друг другу не выставляются — взаимодействуют через публичные интерфейсы.
- Доступ может быть ограничен
- Не нарушит инкапсуляцию
Композиция против наследования
Основная проблема в композиции и наследовании заключается в том, что мир программирования начал думать об этих двух концепциях как о конкурентах. Как и все в разработке программного обеспечения, для каждого из них есть варианты использования и компромиссы, которые необходимо сделать для выбора одного над другим.
Композиция достаточно проста и понятна. Все, что мы видим, состоит из разных частей, например, собака с ногами и хвостом, пейзаж с океаном и небом и т. д. Цель композиции состоит в том, чтобы из частей сделать целое.
Наследование, с другой стороны, является абстракцией. Это простое определение, но оно может быстро стать сложным и запутанным, если использовать его свободно. Цель наследования немного сложнее — чтобы лучше понять ее, полезно взглянуть на цели, которым она служит, семантику и механику.
Семантика наследования
Наследование создает значение или семантику в иерархии классификаций. Упорядочивание атрибутов от более общих к более конкретным и группировка связанных атрибутов в подгруппы. Семантика класса в основном создается в его интерфейсе, то есть в наборе сообщений, на которые класс отвечает.
Но часть семантики также присутствует в наборе сообщений, отправляемых классом. При наследовании от класса вы принимаете право владения всеми сообщениями, которые базовый класс отправляет от вашего имени, а также сообщениями, которые он может получать. Другими словами, подкласс очень тесно связан с базовым классом.
В композиции это не так, потому что подкласс использует только экземпляр базового класса в качестве компонента вместо того, чтобы наследовать атрибуты множества от всего генеалогического древа.
Механика наследования
Наследование создает механику путем кодирования полей и методов класса, чтобы сделать ее многократно используемой и расширяемой в подклассах. Подкласс наследует реализацию базового класса вместе с его интерфейсом.
Таким образом, изменение кода в базовом классе может привести к нежелательным побочным эффектам в нижележащих подклассах или, возможно, даже во всей кодовой базе.
Наиболее распространенное и передовое использование наследования — дифференциальное программирование. Если вам нужен виджет, который точно такой же, как ваши существующие виджеты, за исключением нескольких небольших изменений, наследование сэкономит вам массу времени, потому что вы можете повторно использовать весь интерфейс и реализацию из базового класса.
Однако, если подкласс удаляет то, что существует в базовом классе, наследование вызовет проблемы. Вы можете улучшить производную базового класса, но вы не можете ничего из нее удалить.
Заключительные мысли
Композиция и наследование — это не черно-белая проблема. Бывают моменты, когда лучше использовать один, и моменты, когда требуется другой.
Таким образом, основное различие между композицией и наследованием заключается во взаимоотношениях между объектами.