«Предпочитаю композицию наследованию» - единственная причина защищаться от изменений подписи?

13

Эта страница поддерживает композицию перед наследованием со следующим аргументом (перефразировав его в моих словах):

Изменение в сигнатуре метода суперкласса (который не был переопределен в подклассе) во многих случаях вызывает дополнительные изменения, когда мы используем наследование. Однако, когда мы используем Композицию, необходимые дополнительные изменения происходят только в одном месте: подклассе.

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

Утка
источник
1
см. также: Обсудить этот $ {blog}
gnat
2
Цитата не говорит, что это «единственная» причина.
Тулаинс Кордова
3
Композиция почти всегда лучше, потому что наследование обычно добавляет сложности, связности, чтения кода и снижает гибкость. Но есть заметные исключения, иногда базовый класс или два не повредит. Вот почему его « предпочитают », а не « всегда ».
Марк Роджерс

Ответы:

27

Мне нравятся аналогии, поэтому вот один: вы когда-нибудь видели один из тех телевизоров, в котором был встроенный видеомагнитофон? Как насчет того, что с VCR и DVD-плеером? Или тот, у которого есть Blu-ray, DVD и видеомагнитофон. Ох, и теперь все смотрят в потоковом режиме, поэтому нам нужно создать новый телевизор ...

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

Наследование похоже на телевизор со встроенным видеомагнитофоном. Если у вас есть 3 различных типа поведения, которые вы хотите использовать вместе, и у каждого есть 2 варианта реализации, вам нужно 8 различных классов, чтобы представить все из них. Когда вы добавляете больше опций, числа взрываются. Если вместо этого вы используете композицию, вы избежите этой комбинаторной проблемы, и дизайн будет иметь тенденцию быть более расширяемым.

JimmyJames
источник
6
В конце 90-х и начале 2000-х было много телевизоров с видеомагнитофонами и DVD-плеерами.
JAB
@JAB и многие (большинство?) Телевизоры, проданные сегодня, поддерживают потоковую передачу.
Кейси
4
@JAB И ни один из них не может продать свой DVD-плеер, чтобы помочь купить Bluray-плеер
Александр - Восстановить Монику
1
@JAB Верно. Утверждение «Вы когда-нибудь видели один из тех телевизоров, в котором был встроен видеомагнитофон?» подразумевает, что они существуют.
JimmyJames
4
@Casey Потому что, очевидно, никогда не будет другой технологии, которая заменит эту, верно? Держу пари, что любой из этих телевизоров также поддерживает входы от внешних устройств на тот случай, если кто-то захочет использовать что-то другое, не покупая новый телевизор. Или, скорее, немногие образованные покупатели готовы купить телевизор, в котором отсутствуют такие материалы. Моя предпосылка не в том, что эти подходы не существуют, и я бы не утверждал, что наследования не существует. Дело в том, что такие подходы по своей природе менее гибки, чем композиция.
JimmyJames
4

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

Таким образом, у вас есть несколько объектов, которые предоставляют сервеки. Вы используете их в качестве строительных блоков (на самом деле очень похоже на Lego), чтобы включить другое поведение (например, UserRegistrationService использует службы, предоставляемые EmailerService и UserRepository ).

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

marstato
источник
Я вижу менталитет Java здесь. ООП - это данные, которые упаковываются вместе с функциями, которые на них воздействуют - то, что вы описываете, лучше назвать «модулями». Вы правы в том, что избегать наследования - это хорошая идея, когда вы меняете объекты как модули, но меня беспокоит, что вы, кажется, рекомендуете это как предпочтительный способ использования объектов.
Brilliand
1
@Brilliand Если вы знали только, сколько java-кода написано в том стиле, который вы описываете, и сколько это ужаса ... Smalltalk ввел термин ООП и был разработан для объектов, получающих сообщения. : «Компьютерные вычисления следует рассматривать как внутреннюю возможность объектов, которые могут быть единообразно задействованы при отправке сообщений». Там нет упоминания о данных и функциях, работающих на нем. Извините, но, по-моему, вы серьезно ошибаетесь. Если бы речь шла только о данных и функциях, можно было бы с радостью продолжить написание структур.
Марстато
@ Царь, к сожалению, хотя Java поддерживает статические методы, культура Java не поддерживает
k_g
@Brilliand Кому интересно, что такое ООП? Важно то, как вы можете использовать это и результаты использования его таким образом.
user253751
1
После обсуждения с @Tsar: не нужно создавать экземпляры классов Java, и действительно, такие экземпляры, как UserRegistrationService и EmailService, обычно не следует создавать экземпляры - классы без ассоциированных данных лучше всего работают как статические классы (и, как таковые, не имеют отношения к оригиналу вопрос). Я буду удалять некоторые из моих предыдущих комментариев, но моя первоначальная критика ответа Марсато остается в силе.
Brilliand
4

«Предпочитаю композицию наследованию» - это просто хорошая эвристика

Вы должны учитывать контекст, так как это не универсальное правило. Не принимайте это, чтобы означать, никогда не используйте наследование, когда вы можете сделать композицию. Если бы это было так, вы бы это исправили, запретив наследование.

Я надеюсь прояснить этот момент в этом посте.

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


Пример автомобиля

Я пишу о разработчиках, которые стараются изо всех сил в повествовательных целях

Пойдемте вариант классических примеров , что некоторые курсы ООП использовать ... У нас есть 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 # . Это может помочь вам развязать и облегчить возможность повторного использования и тестирования.

Theraot
источник
Э-э ... .Net Framework использует несколько типов унаследованных потоков для выполнения работы, связанной с потоками. Он работает невероятно хорошо, и он очень прост в использовании и расширении. Ваш пример не очень хороший ...
Т. Сар
1
@ TSAR «это очень легко использовать и расширять». Вы когда-нибудь пытались использовать стандартный поток вывода для отладки? Это был мой единственный опыт, пытаясь расширить их потоки. Это было нелегко. Это был кошмар, который закончился тем, что я явно переопределил каждый метод в классе. Чрезвычайно повторяющийся. И если вы посмотрите на исходный код .NET, явное переопределение всего в значительной степени похоже на то, как они это делают. Так что я не уверен, что это когда-либо так легко расширять.
jpmc26
Чтение и запись структуры из потока лучше выполнять с помощью бесплатных функций или мультиметодов.
user253751
@ jpmc26 Извините, ваш опыт был не очень хорошим. Однако у меня не было проблем с использованием или реализацией потоковых вещей для разных целей.
Т. Сар
3

Обычно движущей идеей Composition-Over-Inheritance является обеспечение большей гибкости проектирования, а не уменьшение количества кода, который необходимо изменить для распространения изменений (что я считаю сомнительным).

Пример в Википедии дает лучший пример силы его подхода:

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

lbenini
источник
Итак, этот раздел дает пример композиции, верно? Я спрашиваю, потому что это не очевидно в статье.
Утку
1
Да, «Композиция над наследованием» не означает, что у вас нет интерфейсов, если вы посмотрите на объект Player, вы увидите, что он хорошо вписывается в иерархию, но код (извините за жестокость) не исходит из наследования, но из композиции с каким-то классом, который делает работу
Ибенини
2

Строка: «Предпочитаю композицию наследованию» - не что иное, как толчок в правильном направлении. Это не спасет вас от вашей собственной глупости и фактически даст вам возможность сделать все намного хуже. Это потому, что здесь много силы.

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

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

На следующий день я спрашиваю, хотел ли это. Как вы думаете, что говорит босс?

Если не было необходимости динамически (во время выполнения) изменять то, что наследовалось от (см. Шаблон состояния), это была огромная трата времени. Как босс может сказать это и все же быть в пользу композиции, а не наследства?

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

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

Направленность очень мощная. Использовать его мудро.

candied_orange
источник
0

Вот еще одна причина: во многих ОО-языках (я имею в виду такие, как C ++, C # или Java здесь), нет способа изменить класс объекта после того, как вы его создали.

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

Затем однажды Инженер повышается до Инженерного менеджера. Вы не можете повторно классифицировать существующего Инженера. Вместо этого вам нужно написать функцию, которая создает Engineering Manager, копирует все данные из Engineer, удаляет Engineer из базы данных, затем добавляет нового Engineering Manager. И вы должны написать такую ​​функцию для каждого возможного изменения роли.

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

Это значительно упрощает создание конкретного класса Employee и рассматривает его как контейнер, в который можно добавлять свойства, например, роль задания.

Саймон Б
источник
Или вы создаете класс Employee с указателем на список ролей, и каждое фактическое задание расширяет роль. Ваш пример может быть сделан, чтобы использовать наследование очень хорошим и разумным способом без излишней работы.
Т. Сар
0

Наследование обеспечивает две вещи:

1) Повторное использование кода: может быть легко выполнено с помощью композиции. И на самом деле, это лучше достигается с помощью композиции, поскольку она лучше сохраняет инкапсуляцию.

2) Полиморфизм: может быть реализован через «интерфейсы» (классы, где все методы чисто виртуальные).

Сильный аргумент в пользу использования неинтерфейсного наследования - это когда количество требуемых методов велико, а ваш подкласс хочет изменить только небольшое их подмножество. Тем не менее, в общем случае ваш интерфейс должен иметь очень маленькие следы, если вы подписываетесь на принцип «единственной ответственности» (вам следует), поэтому такие случаи должны быть редкими.

Наличие стиля кодирования, требующего переопределения всех методов родительского класса, в любом случае не избегает написания большого количества сквозных методов, так почему бы не использовать код повторно через композицию и не получить дополнительное преимущество инкапсуляции? Кроме того, если бы суперкласс был расширен путем добавления другого метода, стиль кодирования потребовал бы добавления этого метода ко всем подклассам (и есть большая вероятность, что мы тогда нарушаем «сегрегацию интерфейса» ). Эта проблема быстро растет, когда вы начинаете иметь несколько уровней наследования.

Наконец, во многих языках модульное тестирование объектов, основанных на «композиции», намного проще, чем модульное тестирование объектов, основанных на «наследовании», путем замены объекта-члена на объект mock / test / dummy.

dlasalle
источник
0

Неужели это единственная причина отдать предпочтение композиции перед наследованием?

Нет.

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

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

Вот как я об этом думаю: когда я читаю чужой код, если я вижу, что они расширили класс, я собираюсь спросить, какое поведение в суперклассе они меняют?

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

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

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

С другой стороны, один из способов выполнения пользовательского рисования - расширить JPanelкласс и переопределить paintComponent()функцию. Это хорошее использование наследства. Если я просматриваю ваш код и вижу, что вы расширили JPanelи переопределили paintComponent()функцию, я точно буду знать, какое поведение вы меняете.

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

Кевин Воркман
источник
Добавление целого класса фабрики с помощью одного метода, который возвращает экземпляр объекта, чтобы вы могли использовать композицию вместо наследования, на самом деле не делает ваш код более читабельным ... (Да, вам это нужно, если вам каждый раз нужен новый экземпляр вызывается конкретный метод.) Это не означает, что наследование хорошо, но композиция не всегда улучшает читабельность.
jpmc26
1
@ jpmc26 ... с какой стати тебе нужен фабричный класс, чтобы создать новый экземпляр класса? И как наследование улучшает читаемость того, что вы описываете?
Кевин Воркман
Разве я не указал вам причину такой фабрики в комментарии выше? Это более читабельно, потому что в результате меньше кода для чтения . Такое же поведение можно выполнить с помощью одного абстрактного метода в базовом классе и нескольких подклассов. (В любом случае, у вас должна была быть своя реализация вашего составного класса.) Если детали конструирования объекта сильно привязаны к базовому классу, он не найдет большого применения где-либо еще в вашем коде, что приведет к дальнейшему сокращению преимущества создания отдельного класса. Затем он держит тесно связанные части кода рядом друг с другом.
jpmc26
Как я уже сказал, это не обязательно означает, что наследование хорошо, но это иллюстрирует, как композиция не улучшает читаемость в некоторых ситуациях.
jpmc26
1
Не видя конкретного примера, трудно действительно комментировать. Но то, что вы описали, звучит как случай чрезмерной инженерии для меня. Нужен экземпляр класса? newКлючевое слово дает один. В любом случае спасибо за обсуждение.
Кевин Воркман