В последние несколько месяцев, мантра «композиция благосклонности вместо наследования», кажется, возникла из ниоткуда и стала почти своего рода мемом в сообществе программистов. И каждый раз, когда я вижу это, я немного озадачен. Это как кто-то сказал: «Скорее тренируйся, чем молотки». По моему опыту, состав и наследование - это два разных инструмента с разными вариантами использования, и рассматривать их так, как если бы они были взаимозаменяемыми, а один по своей сути превосходил другой, не имеет смысла.
Кроме того, я никогда не вижу реального объяснения того, почему наследование плохо, а композиция хороша, что делает меня более подозрительным. Это должно быть просто принято на веру? Подстановка и полиморфизм Лискова обладают хорошо известными, четкими преимуществами, и IMO составляет весь смысл использования объектно-ориентированного программирования, и никто никогда не объясняет, почему их следует отбрасывать в пользу композиции.
Кто-нибудь знает, откуда взялась эта концепция и в чем ее обоснование?
источник
Ответы:
Хотя я думаю, что я слышал обсуждения «состав против наследства» задолго до GoF, я не могу указать на конкретный источник. В любом случае, возможно, это Бух.
<Напыщенная>
Ах, но, как и многие мантры, эта выродилась по типичной линии:
Мем, изначально предназначенный для того, чтобы привести к просветлению, теперь используется как дубинка, чтобы забить их без сознания.
Композиция и наследование очень разные вещи, и их не следует путать друг с другом. Хотя верно, что композицию можно использовать для имитации наследования с большим количеством дополнительной работы , это не делает наследование гражданином второго сорта и не делает композицию любимым сыном. Тот факт, что многие n00bs пытаются использовать наследование в качестве ярлыка, не лишает законной силы механизм, и почти все n00bs учатся на этой ошибке и тем самым улучшают ее.
Пожалуйста, ДУМАЙТЕ о своих проектах, и прекратите извергать лозунги.
</ Декламация>
источник
Опыт.
Как вы говорите, они являются инструментами для разных работ, но эта фраза возникла потому, что люди не использовали ее таким образом.
Наследование - это, прежде всего, полиморфный инструмент, но некоторые люди, к более позднему риску, пытаются использовать его как способ повторного использования / совместного использования кода. Смысл в том, что «хорошо, если я наследую, я получу все методы бесплатно», но игнорируя тот факт, что эти два класса потенциально не имеют полиморфных отношений.
Так зачем отдавать предпочтение композиции, а не наследованию - просто потому, что чаще всего отношения между классами не являются полиморфными. Он существует просто для того, чтобы напомнить людям, чтобы они не реагировали на колено наследственно.
источник
Это не новая идея, я думаю, что она на самом деле была представлена в книге шаблонов дизайна GoF, которая была опубликована в 1994 году.
Основная проблема с наследованием заключается в том, что это белый ящик. По определению вам нужно знать детали реализации класса, от которого вы наследуете. С композицией, с другой стороны, вы заботитесь только об общедоступном интерфейсе создаваемого вами класса.
Из книги GoF:
Статья в Википедии о книге GoF имеет достойное введение.
источник
Чтобы ответить на часть вашего вопроса, я полагаю, что эта концепция впервые появилась в книге « Шаблоны проектирования GOF : элементы многоразового объектно-ориентированного программного обеспечения» , которая была впервые опубликована в 1994 году.
Они предваряют это утверждение кратким сравнением наследования с композицией. Они не говорят «никогда не используйте наследство».
источник
«Композиция над наследованием» - это короткий (и, по-видимому, вводящий в заблуждение) способ сказать «Когда вы чувствуете, что данные (или поведение) класса должны быть включены в другой класс, всегда рассматривайте возможность использования композиции перед слепым применением наследования».
Почему это правда? Поскольку наследование создает тесную связь между двумя классами во время компиляции. Композиция, напротив, является слабой связью, которая, помимо прочего, обеспечивает четкое разделение проблем, возможность переключения зависимостей во время выполнения и более простую, более изолированную тестируемость зависимостей.
Это только означает, что с наследованием следует обращаться осторожно, потому что оно обходится дорого, а не в том, что оно бесполезно. На самом деле, «Композиция по наследованию» часто заканчивается тем, что «Композиция + наследование по наследованию», поскольку вы часто хотите, чтобы ваша составная зависимость была абстрактным суперклассом, а не конкретным подклассом. Это позволяет вам переключаться между различными конкретными реализациями вашей зависимости во время выполнения.
По этой причине (помимо прочего) вы, вероятно, увидите, что наследование используется чаще в форме реализации интерфейса или абстрактных классов, чем ванильное наследование.
(Метафорический) пример может быть:
«У меня есть класс Snake, и я хочу включить в этот класс то, что происходит, когда кусается Змея. Я хотел бы, чтобы Snake унаследовал класс BiterAnimal, имеющий метод Bite (), и переопределил этот метод для отражения ядовитого прикуса. Но Composition over Inheritance предупреждает меня, что я должен попытаться использовать вместо этого композицию ... В моем случае это может привести к тому, что Snake будет иметь член Bite. Класс Bite может быть абстрактным (или интерфейсом) с несколькими подклассами. Это позволит мне нравятся такие вещи, как наличие подклассов VenomousBite и DryBite и возможность изменять прикус в том же экземпляре Snake, когда змея подрастает. Плюс обработка всех эффектов Bite в своем отдельном классе может позволить мне повторно использовать его в этом классе Frost , потому что мороз кусает, но не BiterAnimal, и так далее ... "
источник
Некоторые возможные аргументы для композиции:
Композиция немного более независима от языка / структуры,
и то, что она обеспечивает / требует / разрешает, будет отличаться между языками в зависимости от того, к чему у суб / суперкласса есть доступ и какие последствия для производительности это может иметь в отношении виртуальных методов и т. Д. Композиция довольно проста и требует очень небольшая языковая поддержка, и, следовательно, реализации на разных платформах / каркасах могут более легко использовать шаблоны компоновки.
Композиция - очень простой и тактильный способ построения объектов
Наследование относительно легко понять, но все же не так легко продемонстрировать в реальной жизни. Многие объекты в реальной жизни можно разбить на части и составить. Скажем, велосипед можно построить с помощью двух колес, рамы, сиденья, цепи и т. Д. Легко объяснить композицией. В то время как в метафоре наследования вы могли бы сказать, что велосипед расширяет одноколесный велосипед, несколько выполнимый, но все же намного дальше от реальной картины, чем состав (очевидно, это не очень хороший пример наследования, но суть остается той же). Даже слово «наследование» (по крайней мере, большинства американских носителей английского языка, которое я ожидаю) автоматически вызывает значение в виде строки «Что-то переданное покойным родственником», которое имеет некоторую корреляцию с его значением в программном обеспечении, но все еще не совсем подходит.
Композиция почти всегда более гибкая.
Используя композицию, вы всегда можете определить свое поведение или просто показать эту часть ваших составных частей. Таким образом, вы не сталкиваетесь ни с одним из ограничений, которые могут быть наложены иерархией наследования (виртуальное или не виртуальное и т. Д.)
Таким образом, это может быть потому, что Composition, естественно, является более простой метафорой, которая имеет меньше теоретических ограничений, чем наследование. Кроме того, эти конкретные причины могут быть более очевидными во время разработки или, возможно, проявляться при работе с некоторыми болевыми точками наследования.
Отказ от ответственности:
Очевидно, это не эта четкая / односторонняя улица. Каждый дизайн заслуживает оценки нескольких шаблонов / инструментов. Наследование широко используется, имеет много преимуществ и во много раз более элегантно, чем композиция. Это всего лишь несколько возможных причин, которые можно использовать при выборе композиции.
источник
Возможно, вы только что заметили, как люди говорили это в последние несколько месяцев, но хорошим программистам это известно намного дольше. Я, конечно, говорил об этом где-то около десяти лет.
Суть концепции в том, что наследование сопряжено с большими концептуальными издержками. Когда вы используете наследование, то каждый вызов метода имеет неявную диспетчеризацию. Если у вас есть глубокие деревья наследования, или многократная отправка, или (что еще хуже) и то, и другое, то выяснение, куда будет отправляться конкретный метод в любом конкретном вызове, может стать королевской PITA. Это усложняет правильные рассуждения о коде и усложняет отладку.
Позвольте мне привести простой пример для иллюстрации. Предположим, что глубоко в дереве наследования кто-то назвал метод
foo
. Затем приходит кто-то другой и добавляетfoo
наверху дерева, но делает что-то другое. (Этот случай чаще встречается при множественном наследовании.) Теперь этот человек, работающий с корневым классом, сломал неясный дочерний класс и, вероятно, не осознает этого. Вы можете иметь 100% охват модульными тестами и не заметить эту поломку, потому что человек наверху не будет думать о тестировании дочернего класса, а тесты для дочернего класса не думают о тестировании новых методов, созданных наверху , (По общему признанию, есть способы написать модульные тесты, которые поймут это, но есть также случаи, когда вы не можете легко написать тесты таким образом.)В отличие от этого, когда вы используете композицию, при каждом вызове обычно становится понятнее, на что вы отправляете вызов. (Хорошо, если вы используете инверсию управления, например, с внедрением зависимости, то выяснить, куда идет вызов, также может быть проблематично. Но обычно это проще выяснить.) Это облегчает рассуждение об этом. В качестве бонуса композиция приводит к отделению методов друг от друга. Приведенный выше пример не должен происходить там, потому что дочерний класс переместился бы в какой-то неясный компонент, и никогда не возникает вопрос о том, был ли вызов
foo
предназначен для неясного компонента или основного объекта.Теперь вы абсолютно правы в том, что наследование и композиция - это два совершенно разных инструмента, которые обслуживают два разных типа вещей. Конечно, наследование несет концептуальные накладные расходы, но когда это правильный инструмент для работы, оно несет меньше концептуальных накладных расходов, чем попытка не использовать его и делать вручную то, что оно делает для вас. Никто, кто знает, что они делают, не скажет, что вы никогда не должны использовать наследование. Но будьте уверены, что это правильно.
К сожалению, многие разработчики узнают об объектно-ориентированном программном обеспечении, узнают о наследовании, а затем стараются использовать свой новый топор как можно чаще. Это означает, что они пытаются использовать наследование там, где композиция была правильным инструментом. Надеемся, что со временем они будут лучше учиться, но часто этого не происходит, если после нескольких удаленных конечностей и т. Д. Сказать им заранее, что это плохая идея, приводит к ускорению процесса обучения и снижению травматизма.
источник
Это реакция на наблюдение, что начинающие ОО, как правило, используют наследование, когда им это не нужно. Наследование, конечно, не так уж и плохо, но им можно злоупотреблять. Если один класс просто нуждается в функциональности другого, то, вероятно, будет работать композиция. В других обстоятельствах наследование будет работать, а композиция не будет.
Наследование от класса подразумевает много вещей. Это подразумевает, что Derived является типом Base (см. Принцип подстановки Liskov для подробностей), в котором всякий раз, когда вы используете Base, имеет смысл использовать Derived. Он дает Производный доступ к защищенным членам и функциям-членам Базы. Это тесная связь, то есть она имеет высокую степень связи, и изменения одного из них, скорее всего, потребуют изменений другого.
Сцепление это плохо. Это усложняет понимание и изменение программ. При прочих равных условиях вы всегда должны выбирать опцию с меньшей связью.
Поэтому, если композиция или наследование будут эффективно выполнять эту работу, выберите композицию, поскольку она является более низкой связью. Если композиция не будет эффективно выполнять работу, а наследование будет, выберите наследование, потому что вы должны.
источник
Вот мои два цента (помимо всех превосходных моментов, уже поднятых):
ИМХО, это сводится к тому, что большинство программистов на самом деле не получают наследство и в итоге перестараются, частично в результате того, как преподается эта концепция. Эта концепция существует как способ отговорить их от переусердствования, вместо того, чтобы сосредоточиться на том, чтобы научить их делать это правильно.
Любой, кто потратил время на преподавание или наставничество, знает, что это часто происходит, особенно с новыми разработчиками, которые имеют опыт работы в других парадигмах:
Эти разработчики изначально считают, что наследование - это страшная концепция. Таким образом, они заканчивают тем, что создавали типы с интерфейсными перекрытиями (например, с тем же заданным поведением без общих подтипов) и с глобальными переменными для реализации общих частей функциональности.
Затем (часто в результате чрезмерного усердного обучения) они узнают о преимуществах наследования, но это часто преподается как универсальное решение для повторного использования. В итоге у них возникает ощущение, что любое общее поведение должно передаваться по наследству. Это связано с тем, что основное внимание часто уделяется наследованию реализации, а не подтипам.
В 80% случаев это достаточно хорошо. Но остальные 20% находятся там, где начинается проблема. Во избежание переписывания и для обеспечения того, чтобы они воспользовались преимуществами совместного использования реализации, они начинают проектировать свою иерархию вокруг предполагаемой реализации, а не базовых абстракций. «Стек наследует из двусвязного списка» является хорошим примером этого.
Большинство учителей не настаивают на представлении концепции интерфейсов на этом этапе, потому что это либо другая концепция, либо потому что (в C ++) вы должны притворяться, используя абстрактные классы и множественное наследование, которое не преподается на данном этапе. В Java многие учителя не различают «отсутствие множественного наследования» или «множественное наследование - это зло» от важности интерфейсов.
Все это усугубляется тем фактом, что мы так много узнали о том, что нет необходимости писать лишний код с наследованием реализации, что тонны простого кода делегирования выглядят неестественно. Таким образом, композиция выглядит неопрятно, поэтому нам нужны эти правила, чтобы подтолкнуть новых программистов к их использованию (что они и переусердствовали).
источник
В одном из комментариев Мейсон упомянул, что однажды мы будем говорить о наследовании, которое считается вредным .
Я надеюсь, что это так.
Проблема с наследованием одновременно проста и смертельна, она не учитывает идею, что одна концепция должна иметь одну функциональность.
В большинстве ОО-языков при наследовании от базового класса вы:
И тут становятся неприятности.
Это не единственный подход, хотя 00-языковые версии в основном придерживаются его. К счастью, интерфейсы / абстрактные классы существуют в тех.
Кроме того, отсутствие простоты для выполнения других действий способствует его широкому использованию: честно говоря, даже зная об этом, вы бы унаследовали от интерфейса и встраивали базовый класс по составу, делегируя большинство вызовов метода? Хотя было бы гораздо лучше, вы даже были бы предупреждены, если вдруг в интерфейсе появился новый метод, и ему пришлось бы сознательно выбирать, как его реализовать.
В качестве контрапункта
Haskell
разрешается использовать принцип Лискова только при «выводе» из чистых интерфейсов (так называемых концепций) (1). Вы не можете наследовать от существующего класса, только композиция позволяет встраивать его данные.(1) концепции могут обеспечить разумное значение по умолчанию для реализации, но, поскольку у них нет данных, это значение по умолчанию может быть определено только в терминах других методов, предложенных концепцией, или в терминах констант.
источник
Ответ прост: наследование имеет большую связь, чем состав. Учитывая два варианта, в других отношениях эквивалентных качеств, выберите тот, который менее связан.
источник
Я думаю, что такой совет похож на высказывание «предпочитаю водить, чем летать». То есть самолеты имеют все виды преимуществ перед автомобилями, но это сопряжено с определенной сложностью. Поэтому, если многие люди пытаются лететь из центра города в пригород, совет, который им действительно нужно услышать, заключается в том, что им не нужно летать, и на самом деле, полет просто усложнит в долгосрочной перспективе, даже если это кажется круто / эффективно / легко в краткосрочной перспективе. Принимая во внимание , когда вы действительно должны летать, это вообще должно быть очевидно.
Точно так же наследование может делать то, чего не может композиция, но вы должны использовать это, когда вам это нужно, а не когда вы этого не делаете. Так что, если у вас никогда не возникает соблазн предположить, что вам нужно наследование, когда вы этого не делаете, тогда вам не нужен совет «предпочесть композицию». Но многие люди делают , и делать нужно это совет.
Предполагается, что когда вам действительно нужно наследование, это очевидно, и вы должны использовать его тогда.
Кроме того, ответ Стивена Лоу. Действительно, действительно это.
источник
Наследование не является плохим по своей природе, а композиция не является хорошей. Это просто инструменты, которые OO-программист может использовать для разработки программного обеспечения.
Когда вы смотрите на класс, он делает больше, чем должен ( SRP )? Является ли это дублированием функциональности без необходимости ( СУХОЙ ) или чрезмерно заинтересован в свойствах или методах других классов ( Feature Envy )? Если класс нарушает все эти концепции (и, возможно, даже больше), он пытается стать классом Бога . Это целый ряд проблем, которые могут возникнуть при разработке программного обеспечения, но ни одна из которых не обязательно является проблемой наследования, но может быстро создать серьезные головные боли и хрупкие зависимости, где также применяется полиморфизм.
Корень проблемы, скорее всего, заключается не в недостаточном понимании наследования, а скорее в том, что он либо неудачен с точки зрения дизайна, либо, возможно, не распознает «запахи кода», относящиеся к классам, которые не придерживаются принципа единой ответственности . Полиморфизм и замещение Лискова не нужно отбрасывать в пользу композиции. Сам полиморфизм может быть применен, не полагаясь на наследование, все это довольно взаимодополняющие понятия. Если применять вдумчиво. Хитрость заключается в том, чтобы сохранить ваш код простым и чистым и не поддаваться чрезмерной озабоченности количеством классов, которые вам нужно создать для создания надежной конструкции системы.
Что касается вопроса о предпочтении композиции перед наследованием, то это на самом деле просто еще один случай вдумчивого применения элементов дизайна, которые имеют наибольшее значение с точки зрения решаемой проблемы. Если вам не нужно наследовать поведение, то вам, вероятно, не следует этого делать, поскольку композиция поможет избежать несовместимости и серьезных усилий по рефакторингу в дальнейшем. Если, с другой стороны, вы обнаружите, что вы повторяете много кода, так что все дублирование сосредоточено на группе похожих классов, возможно, создание общего предка поможет уменьшить количество идентичных вызовов и классов. Возможно, вам придется повторить между каждым классом. Таким образом, вы предпочитаете композицию, но не предполагаете, что наследование никогда не применимо.
источник
Я полагаю, что настоящая цитата взята из «Эффективной Явы» Джошуа Блоха, где она называется одной из глав. Он утверждает, что наследование безопасно в пакете и там, где класс был специально разработан и задокументирован для расширения. Как утверждают другие, он утверждает, что наследование нарушает инкапсуляцию, поскольку подкласс зависит от деталей реализации его суперкласса. Это приводит, по его словам, к хрупкости.
Хотя он не упоминает об этом, мне кажется, что одиночное наследование в Java означает, что композиция дает вам гораздо большую гибкость, чем наследование.
источник