Эта страница поддерживает композицию перед наследованием со следующим аргументом (перефразировав его в моих словах):
Изменение в сигнатуре метода суперкласса (который не был переопределен в подклассе) во многих случаях вызывает дополнительные изменения, когда мы используем наследование. Однако, когда мы используем Композицию, необходимые дополнительные изменения происходят только в одном месте: подклассе.
Неужели это единственная причина отдать предпочтение композиции перед наследованием? Потому что, если это так, эту проблему можно легко решить, применяя стиль кодирования, который поддерживает переопределение всех методов суперкласса, даже если подкласс не меняет реализацию (то есть, помещает фиктивные переопределения в подкласс). Я что-то здесь упускаю?
Ответы:
Мне нравятся аналогии, поэтому вот один: вы когда-нибудь видели один из тех телевизоров, в котором был встроенный видеомагнитофон? Как насчет того, что с VCR и DVD-плеером? Или тот, у которого есть Blu-ray, DVD и видеомагнитофон. Ох, и теперь все смотрят в потоковом режиме, поэтому нам нужно создать новый телевизор ...
Скорее всего, если у вас есть телевизор, у вас нет такого, как указано выше. Вероятно, большинство из них никогда не существовало. Скорее всего, у вас есть монитор или набор с множеством входов, поэтому вам не нужно новый набор каждый раз, когда появляется новый тип устройства ввода.
Наследование похоже на телевизор со встроенным видеомагнитофоном. Если у вас есть 3 различных типа поведения, которые вы хотите использовать вместе, и у каждого есть 2 варианта реализации, вам нужно 8 различных классов, чтобы представить все из них. Когда вы добавляете больше опций, числа взрываются. Если вместо этого вы используете композицию, вы избежите этой комбинаторной проблемы, и дизайн будет иметь тенденцию быть более расширяемым.
источник
Нет это не так. По моему опыту, составление над наследованием является результатом того, что ваше программное обеспечение воспринимается как совокупность объектов с поведением, которые взаимодействуют друг с другом / объектами, которые предоставляют услуги друг другу вместо классов и данных .
Таким образом, у вас есть несколько объектов, которые предоставляют сервеки. Вы используете их в качестве строительных блоков (на самом деле очень похоже на Lego), чтобы включить другое поведение (например, UserRegistrationService использует службы, предоставляемые EmailerService и UserRepository ).
При построении больших систем с таким подходом я, естественно, использовал наследование в очень немногих случаях. В конце концов, наследование является очень мощным инструментом, который следует использовать с осторожностью и только там, где это необходимо.
источник
«Предпочитаю композицию наследованию» - это просто хорошая эвристика
Вы должны учитывать контекст, так как это не универсальное правило. Не принимайте это, чтобы означать, никогда не используйте наследование, когда вы можете сделать композицию. Если бы это было так, вы бы это исправили, запретив наследование.
Я надеюсь прояснить этот момент в этом посте.
Я не буду пытаться защищать достоинства композиции сам по себе. Который я считаю не по теме. Вместо этого я расскажу о некоторых ситуациях, когда разработчик может рассмотреть наследование, которое было бы лучше при использовании композиции. На этом примечании наследование имеет свои достоинства, которые я также считаю не по теме.
Пример автомобиля
Я пишу о разработчиках, которые стараются изо всех сил в повествовательных целях
Пойдемте вариант классических примеров , что некоторые курсы ООП использовать ... У нас есть
Vehicle
класс, то мы получимCar
,Airplane
,Balloon
иShip
.Примечание . Если вам нужно обосновать этот пример, представьте, что это такие объекты в видеоигре.
Тогда
Car
иAirplane
может иметь какой-то общий код, потому что они оба могут кататься на колесе по земле. Разработчики могут рассмотреть возможность создания промежуточного класса в цепочке наследования для этого. Тем не менее, на самом деле есть и некоторый общий код междуAirplane
иBalloon
. Для этого они могут рассмотреть возможность создания другого промежуточного класса в цепочке наследования.Таким образом, разработчик будет искать множественное наследование. В тот момент, когда разработчики ищут множественное наследование, дизайн уже ошибся.
Лучше моделировать это поведение как интерфейсы и композицию, поэтому мы можем использовать его повторно, не сталкиваясь с наследованием нескольких классов. Если разработчики, например, создают
FlyingVehicule
класс. Они будут говорить, чтоAirplane
этоFlyingVehicule
(наследование классов), но мы могли бы вместо этого сказать, что оноAirplane
имеетFlying
компонент (состав) иAirplane
являетсяIFlyingVehicule
(наследование интерфейса).Используя интерфейсы, при необходимости мы можем иметь множественное наследование (интерфейсов). Кроме того, вы не привязаны к конкретной реализации. Повышение возможности повторного использования и тестирования вашего кода.
Помните, что наследование является инструментом полиморфизма. Кроме того, полиморфизм является инструментом для повторного использования. Если вы можете увеличить возможность повторного использования вашего кода с помощью композиции, то сделайте это. Если вы не уверены, какая композиция или нет обеспечивает лучшую возможность повторного использования, «Предпочитать композицию перед наследованием» будет хорошей эвристикой.
Все это без упоминания
Amphibious
.На самом деле, нам могут не понадобиться вещи, которые оторвутся от земли. У Стивена Херна есть более красноречивый пример в его статьях «Композиция в пользу наследования», часть 1 и часть 2 .
Заменимость и инкапсуляция
Должны
A
наследовать или сочинятьB
?Если
A
это специализация,B
которая должна соответствовать принципу подстановки Лискова , наследование жизнеспособно, даже желательно. Если есть ситуации, когдаA
заменаB
недопустима, мы не должны использовать наследование.Возможно, нас заинтересует композиция как форма защитного программирования для защиты производного класса . В частности, как только вы начнете использовать его
B
для других различных целей, может возникнуть необходимость изменить или расширить его, чтобы он больше подходил для этих целей. Если есть риск, которыйB
может подвергнуть методы, которые могут привести к недопустимому состоянию,A
мы должны использовать композицию вместо наследования. Даже если мы являемся авторомB
и тогоA
, и другого, беспокоиться не о чем, поэтому композиция облегчает повторное использованиеB
.Мы можем даже утверждать, что если есть функции, в
B
которыхA
нет необходимости (и мы не знаем, может ли эти функции привести к недопустимому состояниюA
, как в настоящей реализации, так и в будущем), будет хорошей идеей использовать композицию вместо наследства.Композиция также имеет такие преимущества, как возможность переключения реализаций и упрощение насмешек.
Примечание : есть ситуации, когда мы хотим использовать композицию, несмотря на то, что замена действительна. Мы заархивируем эту заменяемость , используя интерфейсы или абстрактные классы (которые следует использовать, когда это другая тема), а затем используем композицию с внедрением зависимостей реальной реализации.
Наконец, конечно, есть аргумент, что мы должны использовать композицию для защиты родительского класса, потому что наследование нарушает инкапсуляцию родительского класса:
- Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения, Gang of Four
Ну, это плохо разработанный родительский класс. Вот почему вы должны:
- Эффективная Java, Джош Блох
Йо-йо проблема
Другой случай, когда помогает композиция - это проблема Йо-йо . Это цитата из Википедии:
Например, вы можете решить: ваш класс
C
не будет наследоваться от классаB
. Вместо этого ваш классC
будет иметь член типаA
, который может быть или не быть (или иметь) объект типаB
. Таким образом, вы будете программировать не против деталей реализацииB
, а против контракта, предлагаемого интерфейсомA
.Счетчик примеров
Многие фреймворки предпочитают наследование по составу (что противоположно тому, что мы обсуждали). Разработчик может сделать это, потому что он вложил много работы в свой базовый класс, чтобы его реализация с использованием композиции увеличила бы размер клиентского кода. Иногда это связано с ограничениями языка.
Например, среда PHP ORM может создать базовый класс, который использует магические методы, чтобы позволить писать код, как если бы объект имел реальные свойства. Вместо этого код, обрабатываемый магическими методами, будет отправляться в базу данных, запрашивать конкретное запрошенное поле (возможно, кешировать его для будущего запроса) и возвращать его. Выполнение этого с композицией потребует от клиента либо создания свойств для каждого поля, либо написания некоторой версии кода магических методов.
Приложение : Есть несколько других способов расширения объектов ORM. Таким образом, я не думаю, что наследование необходимо в этом случае. Это дешевле.
В другом примере движок видеоигры может создать базовый класс, который использует собственный код в зависимости от целевой платформы для выполнения 3D-рендеринга и обработки событий. Этот код сложен и зависит от платформы. В действительности, для пользователя-разработчика движка было бы дорого и подвержено ошибкам иметь дело с этим кодом, что является частью причины использования движка.
Кроме того, без части 3D-рендеринга, это то, сколько работает каркас виджетов. Это избавляет вас от беспокойства по поводу обработки сообщений ОС… на самом деле, во многих языках вы не можете писать такой код без какой-либо формы родного связывания. Более того, если бы вы сделали это, это ограничило бы вашу мобильность. Вместо этого, с наследованием, при условии, что разработчик не нарушает совместимость (слишком много); в будущем вы сможете легко перенести свой код на любые новые платформы, которые они поддерживают.
Кроме того, учтите, что много раз мы хотим переопределить только несколько методов и оставить все остальное с реализациями по умолчанию. Если бы мы использовали композицию, нам пришлось бы создавать все эти методы, даже если бы они были делегированы для обернутого объекта.
Благодаря этому аргументу существует точка, в которой композиция может быть худшей для удобства обслуживания, чем наследования (когда базовый класс слишком сложен). Тем не менее, помните, что поддерживаемость наследования может быть хуже, чем у композиции (когда дерево наследования слишком сложное), что я и упоминаю в проблеме йо-йо.
В представленных примерах разработчики редко намерены повторно использовать код, сгенерированный посредством наследования, в других проектах. Это смягчает снижение возможности повторного использования наследования вместо композиции. Кроме того, используя наследование, разработчики фреймворка могут предоставить множество простых в использовании и легко обнаружить код.
Последние мысли
Как видите, в некоторых ситуациях композиция имеет некоторое преимущество перед наследованием, но не всегда. Для принятия решения важно учитывать контекст и различные вовлеченные факторы (такие как возможность повторного использования, ремонтопригодность, тестируемость и т. Д.). Возвращаясь к первому пункту: «Предпочитаю композицию наследованию» - это просто хорошая эвристика.
Вы также можете заметить, что многие из описанных мною ситуаций могут быть в некоторой степени разрешены с помощью черт или миксинов. К сожалению, это не общие черты в большом списке языков, и они обычно идут с некоторыми затратами на производительность. К счастью, их популярные методы расширения двоюродного брата и реализации по умолчанию смягчают некоторые ситуации.
У меня есть недавняя статья, в которой я рассказываю о некоторых преимуществах интерфейсов, почему нам нужны интерфейсы между пользовательским интерфейсом, бизнесом и доступом к данным в C # . Это может помочь вам развязать и облегчить возможность повторного использования и тестирования.
источник
Обычно движущей идеей Composition-Over-Inheritance является обеспечение большей гибкости проектирования, а не уменьшение количества кода, который необходимо изменить для распространения изменений (что я считаю сомнительным).
Пример в Википедии дает лучший пример силы его подхода:
Определяя базовый интерфейс для каждого «составного элемента», вы можете легко создавать различные «составные объекты» с разнообразием, которое может быть труднодостижимо только с помощью наследования.
источник
Строка: «Предпочитаю композицию наследованию» - не что иное, как толчок в правильном направлении. Это не спасет вас от вашей собственной глупости и фактически даст вам возможность сделать все намного хуже. Это потому, что здесь много силы.
Основная причина, по которой это нужно сказать, заключается в том, что программисты ленивы. Настолько ленивым, что они примут любое решение, которое означает меньше набора с клавиатуры. Это основной пункт продажи наследства. Сегодня я печатаю меньше, а завтра кто-то еще облажается. Ну и что, я хочу домой.
На следующий день начальник настаивает на том, чтобы я использовал композицию. Не объясняет почему, но настаивает на этом. Поэтому я беру пример того, от чего я унаследовал, выставляю интерфейс, идентичный тому, от которого я унаследовал, и реализую этот интерфейс, делегируя всю работу той вещи, от которой я унаследовал. Это все, что можно набрать с мертвой точки, что можно было бы сделать с помощью инструмента рефакторинга, и это кажется мне бессмысленным, но босс хотел, чтобы я это сделал.
На следующий день я спрашиваю, хотел ли это. Как вы думаете, что говорит босс?
Если не было необходимости динамически (во время выполнения) изменять то, что наследовалось от (см. Шаблон состояния), это была огромная трата времени. Как босс может сказать это и все же быть в пользу композиции, а не наследства?
В дополнение к этому не предпринимается никаких действий по предотвращению поломки из-за изменений сигнатуры метода, что полностью пропускает самое большое преимущество композиции. В отличие от наследования вы можете создать другой интерфейс . Еще один уместно, как это будет использоваться на этом уровне. Вы знаете, абстракция!
У автоматической замены наследования составлением и делегированием есть некоторые незначительные преимущества (и некоторые недостатки), но при отключении вашего ума во время этого процесса упускается огромная возможность.
Направленность очень мощная. Использовать его мудро.
источник
Вот еще одна причина: во многих ОО-языках (я имею в виду такие, как C ++, C # или Java здесь), нет способа изменить класс объекта после того, как вы его создали.
Предположим, мы создаем базу данных сотрудников. У нас есть абстрактный базовый класс Employee, и мы начинаем выводить новые классы для конкретных ролей - инженер, охранник, водитель грузовика и так далее. Каждый класс имеет специальное поведение для этой роли. Все хорошо.
Затем однажды Инженер повышается до Инженерного менеджера. Вы не можете повторно классифицировать существующего Инженера. Вместо этого вам нужно написать функцию, которая создает Engineering Manager, копирует все данные из Engineer, удаляет Engineer из базы данных, затем добавляет нового Engineering Manager. И вы должны написать такую функцию для каждого возможного изменения роли.
Еще хуже, предположим, что водитель грузовика уходит в длительный отпуск по болезни, а охранник предлагает заняться вождением грузовика неполный рабочий день, чтобы заполнить его. Теперь вам нужно изобрести новый класс охранника и водителя грузовика только для них.
Это значительно упрощает создание конкретного класса Employee и рассматривает его как контейнер, в который можно добавлять свойства, например, роль задания.
источник
Наследование обеспечивает две вещи:
1) Повторное использование кода: может быть легко выполнено с помощью композиции. И на самом деле, это лучше достигается с помощью композиции, поскольку она лучше сохраняет инкапсуляцию.
2) Полиморфизм: может быть реализован через «интерфейсы» (классы, где все методы чисто виртуальные).
Сильный аргумент в пользу использования неинтерфейсного наследования - это когда количество требуемых методов велико, а ваш подкласс хочет изменить только небольшое их подмножество. Тем не менее, в общем случае ваш интерфейс должен иметь очень маленькие следы, если вы подписываетесь на принцип «единственной ответственности» (вам следует), поэтому такие случаи должны быть редкими.
Наличие стиля кодирования, требующего переопределения всех методов родительского класса, в любом случае не избегает написания большого количества сквозных методов, так почему бы не использовать код повторно через композицию и не получить дополнительное преимущество инкапсуляции? Кроме того, если бы суперкласс был расширен путем добавления другого метода, стиль кодирования потребовал бы добавления этого метода ко всем подклассам (и есть большая вероятность, что мы тогда нарушаем «сегрегацию интерфейса» ). Эта проблема быстро растет, когда вы начинаете иметь несколько уровней наследования.
Наконец, во многих языках модульное тестирование объектов, основанных на «композиции», намного проще, чем модульное тестирование объектов, основанных на «наследовании», путем замены объекта-члена на объект mock / test / dummy.
источник
Нет.
Есть много причин, чтобы отдать предпочтение композиции, а не наследованию. Там нет ни одного правильного ответа. Но я добавлю еще одну причину, чтобы отдать предпочтение композиции, а не наследованию:
Использование композиции по сравнению с наследованием улучшает читабельность вашего кода , что очень важно при работе над большим проектом с несколькими людьми.
Вот как я об этом думаю: когда я читаю чужой код, если я вижу, что они расширили класс, я собираюсь спросить, какое поведение в суперклассе они меняют?
А потом я просмотрю их код в поисках переопределенной функции. Если я не вижу ничего, я классифицирую это как злоупотребление наследованием. Вместо этого они должны были использовать композицию, чтобы спасти меня (и всех остальных членов команды) от 5 минут чтения их кода!
Вот конкретный пример: в Java
JFrame
класс представляет окно. Чтобы создать окно, вы создаете экземплярJFrame
и затем вызываете функции для этого экземпляра, чтобы добавить к нему материал и показать его.Многие учебные пособия (даже официальные) рекомендуют расширять,
JFrame
чтобы вы могли рассматривать экземпляры вашего класса как экземплярыJFrame
. Но имхо (и мнение многих других) заключается в том, что это довольно плохая привычка! Вместо этого вы должны просто создать экземплярJFrame
и вызвать функции для этого экземпляра. В этом случае абсолютно нет необходимости расширятьJFrame
, так как вы не меняете поведение родительского класса по умолчанию.С другой стороны, один из способов выполнения пользовательского рисования - расширить
JPanel
класс и переопределитьpaintComponent()
функцию. Это хорошее использование наследства. Если я просматриваю ваш код и вижу, что вы расширилиJPanel
и переопределилиpaintComponent()
функцию, я точно буду знать, какое поведение вы меняете.Если вы сами пишете код, то использовать наследование может быть так же просто, как и композицию. Но представьте, что вы находитесь в групповой среде, и что другие люди будут читать ваш код. Использование композиции вместо наследования облегчает чтение вашего кода.
источник
new
Ключевое слово дает один. В любом случае спасибо за обсуждение.