Откуда взялась эта концепция «вкусовой композиции над наследованием»?

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

Кроме того, я никогда не вижу реального объяснения почему наследование плохое, а состав хороший, что только делает меня более подозрительным. Предполагается ли, что его принимают только на веру? Лизковская подстановка и полиморфизм имеют хорошо известные, четкие преимущества, а ИМО включает в себя всю цель использования объектно-ориентированного программирования, и никто никогда не объясняет, почему их следует отбрасывать в пользу композиции.

Кто-нибудь знает, откуда взялось это понятие, и каково его обоснование?

134 голоса | спросил Mason Wheeler 1 Jam1000000amThu, 01 Jan 1970 03:00:00 +030070 1970, 03:00:00

14 ответов


128

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

& л; напыщенная >

Ah, но, как и многие мантры, он выродился по типичным линиям:

  1. он вводится с подробным объяснением и аргументом уважаемого источника, который монетизирует флеш-память как напоминание о первоначальном сложном обсуждении
  2. он делится со знанием «частичного клуба» несколькими известными в течение некоторого времени, обычно при комментировании ошибок n00b
  3. в скором времени это бессмысленно повторяется тысячами и тысячами, которые никогда не читают объяснения, но любят использовать его в качестве оправдания, чтобы не думать , и как дешевый и простой способ чувствовать себя лучше других li>
  4. В конце концов, никакое разумное развенчивание не может остановить поток «мема» - и парадигма вырождается в религию и догму.

Мем, изначально предназначенный для того, чтобы привести n00bs к просветлению, теперь используется как клуб, чтобы обманывать их без сознания.

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

Пожалуйста, ДУМАЙТЕ о ваших проектах и ​​прекратите лозунги ломать.

& л; /& напыщенная GT;

ответил Josh S 11 Mayam14 2014, 05:23:55
71

Опыт.

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

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

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

ответил Josh S 11 Mayam14 2014, 05:23:55
40

Это не новая идея, я считаю, что она была фактически представлена ​​в книге шаблонов дизайна GoF, которая была опубликована в 1994 году.

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

Из книги GoF:

  

Inheritance предоставляет подкласс для подробностей реализации его родителя, часто говорят, что «наследование прерывает инкапсуляцию»

Статья в Википедии в книге GoF имеет достойное введение.

ответил Josh S 11 Mayam14 2014, 05:23:55
26

Чтобы ответить на часть вашего вопроса, я считаю, что эта концепция впервые появилась в GOF шаблоны проектирования: элементы многоразового объектно-ориентированного Software , которая была впервые опубликована в 1994 году. Фраза появляется в верхней части страницы 20 во введении:

  

Использовать композицию объекта над наследованием

Они предисловили это утверждение кратким сравнением наследования с композицией. Они не говорят «никогда не использовать наследование».

ответил Josh S 11 Mayam14 2014, 05:23:55
22

Некоторые возможные аргументы для композиции:

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

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

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

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

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

ответил Josh S 11 Mayam14 2014, 05:23:55
21

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

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

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

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

A (метафорический) пример может быть:

«У меня есть класс Snake, и я хочу включить его в качестве части этого класса, что происходит, когда Snake укусов. У меня возникнет соблазн заставить Snake наследовать класс BiterAnimal, у которого есть метод Bite (), и переопределить этот метод для отражают ядовитый укус, но композиция над наследованием предупреждает меня, что я должен попытаться использовать композицию вместо этого ... В моем случае это может привести к тому, что Змея имеет член Bite. Класс Bite может быть абстрактным (или интерфейсом) с несколькими подклассами. Это дало бы мне приятные вещи, такие как наличие подклассов VenomousBite и DryBite и возможность перекусить на одном и том же экземпляре Snake, поскольку змея возрастает. Кроме того, обработка всех эффектов Bite в отдельном классе может позволить мне повторно использовать его в этот класс Frost, потому что мороз укусов, но не является BiterAnimal и так далее ... "

ответил Josh S 11 Mayam14 2014, 05:23:55
10

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

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

Позвольте мне привести простой пример для иллюстрации. Предположим, что в глубине дерева наследования кто-то назвал метод foo. Затем появляется кто-то другой и добавляет foo в верхнюю часть дерева, но делает что-то другое. (Этот случай чаще встречается при множественном наследовании.) Теперь этот человек, работающий в корневом классе, сломал неясный дочерний класс и, вероятно, этого не осознает. Вы можете иметь 100% -ый охват с помощью единичных тестов и не заметить этот полом, потому что человек наверху не будет думать о тестировании дочернего класса, а тесты для дочернего класса не предполагают тестирования новых методов, созданных в верхней части , (По общему признанию, есть способы написать модульные тесты, которые поймают это, но есть также случаи, когда вы не можете легко написать тесты таким образом.)

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

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

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

ответил Josh S 11 Mayam14 2014, 05:23:55
8

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

Наследование от класса подразумевает много вещей. Это подразумевает, что Derived - это тип Base (см. принцип замены Лискова для подробностей), в том случае, когда вы используйте базу, имеет смысл использовать Derived. Он дает Derived доступ к защищенным членам и членам функции Base. Это близкая связь, то есть у нее высокая связь, и изменения в ней, скорее всего, потребуют изменений в другой.

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

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

ответил Josh S 11 Mayam14 2014, 05:23:55
8

Вот мои два цента (помимо всех превосходных очков, которые уже подняты):

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

Любой, кто потратил время на обучение или наставничество, знает, что это часто случается, особенно с новыми разработчиками, которые имеют опыт в других парадигмах:

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

Затем (часто в результате чрезмерного обучения) они узнают о преимуществах наследования, но его часто учат как универсальное решение для повторного использования. Они получают представление о том, что любое совместное поведение должно делиться через наследование. Это связано с тем, что основное внимание уделяется наследованию реализации, а не подтипированию.

В 80% случаев это достаточно хорошо. Но другие 20% - вот где проблема начинается. Чтобы избежать перезаписи и убедиться, что они воспользовались совместным внедрением, они начинают разрабатывать свою иерархию вокруг предполагаемой реализации, а не лежат в основе абстракций. Хорошим примером этого является «Stack наследует от двусвязного списка».

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

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

ответил Josh S 11 Mayam14 2014, 05:23:55
8

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

Надеюсь, что так.

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

В большинстве OO-языков при наследовании от базового класса вы:

  • наследует от своего интерфейса
  • наследует от его реализации (как данные, так и методы)

И вот эта проблема.

Это not единственный подход, хотя 00-языки в основном застряли в нем. К счастью, интерфейсы /абстрактные классы существуют в них.

Кроме того, отсутствие непринужденности для других способствовало тому, что оно в значительной степени использовалось: честно говоря, даже зная это, вы бы наследовали бы от интерфейса и встраивали базовый класс по составу, делегируя большинство вызовов методов? Было бы намного лучше, хотя бы вас даже предупредили, если вдруг новый метод появится в интерфейсе и ему придется выбирать, сознательно, как его реализовать.

Как контр-точка, Haskell позволяет использовать принцип Лискова только при «выводе» из чистых интерфейсов (называемых концепциями) (1). Вы не можете получить из существующего класса, только композиция позволяет вам вставлять свои данные.

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

ответил Josh S 11 Mayam14 2014, 05:23:55
7

Простой ответ: наследование имеет большее сцепление, чем состав. Учитывая два варианта, эквивалентных качеств, выберите тот, который менее связан.

ответил Josh S 11 Mayam14 2014, 05:23:55
7

Я думаю, что такой совет напоминает высказывание «предпочитают вождение летать». То есть, самолеты имеют всевозможные преимущества перед автомобилями, но это имеет определенную сложность. Поэтому, если многие люди пытаются лететь из центра города в пригород, совет, который им действительно нравится, действительно должен услышать, что им не нужно летать, и на самом деле, полеты просто сделают это более сложный в долгосрочной перспективе, даже если он кажется прохладным /эффективным /легким в краткосрочной перспективе. Если вы do должны летать, это обычно должно быть очевидно.

Аналогично, наследование может делать вещи, которые не может быть составлено, но вы должны использовать это, когда вам это нужно, а не когда вы этого не сделаете. Поэтому, если у вас никогда не возникает соблазна просто предположить, что вам нужно наследование, когда вы этого не сделаете, вам не нужен совет «предпочитают композицию». Но многим людям do и do нужен этот совет.

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

Кроме того, ответ Стивена Лоу. На самом деле, действительно.

ответил Josh S 11 Mayam14 2014, 05:23:55
2

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

Когда вы смотрите на класс, он делает больше, чем нужно ( SRP ) ? Является ли он дублированием функций без необходимости ( DRY ), или он слишком заинтересован в свойствах или методах других классы ( Зависть Feature ). Если класс нарушает все эти понятия (и, возможно, больше), он пытается быть God Class . Это ряд проблем, которые могут возникать при разработке программного обеспечения, ни одна из которых не является проблемой наследования, но которая может быстро создавать основные головные боли и хрупкие зависимости, в которых также применяется полиморфизм.

Корень проблемы, вероятно, будет меньше недостатка в понимании наследования и более одного из либо плохого выбора с точки зрения дизайна, либо, возможно, не распознавания «запахов кода», относящихся к классам, которые не придерживаются Принцип единой ответственности . Полиморфизм и Замена Лискова не нужно отбрасывать в пользу композиции. Сам полиморфизм можно применять, не полагаясь на наследование, все это довольно бесплатные концепции. Если применить задумчиво. Хитрость заключается в том, чтобы держать ваш код простым и чистым и не поддаваться чрезмерному беспокойству о количестве классов, которые вам нужно создать, чтобы создать надежный дизайн системы.

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

ответил Josh S 11 Mayam14 2014, 05:23:55
-1

Фактическая цитата приходит, я считаю, из «Эффективной Java» Джошуа Блоха, где это название одной из глав. Он утверждает, что наследование безопасно в пакете и везде, где класс был специально разработан и документирован для расширения. Он утверждает, что, как отмечали другие, наследование прерывает инкапсуляцию, потому что подкласс зависит от деталей реализации его суперкласса. Это приводит, по его словам, к хрупкости.

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

ответил Josh S 11 Mayam14 2014, 05:23:55

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132