Наследование против агрегации [закрыто]


151

Существует два подхода к расширению, расширению и повторному использованию кода в объектно-ориентированной системе:

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

  2. Агрегация: создайте новую функциональность, беря другие классы и объединяя их в новый класс. Присоедините общий интерфейс к этому новому классу для взаимодействия с другим кодом.

Каковы преимущества, затраты и последствия каждого? Есть ли другие альтернативы?

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


9
Хороший вопрос, к сожалению у меня сейчас мало времени.
Toon Krijthe

4
Хороший ответ лучше, чем более быстрый ... Я буду следить за своим вопросом, поэтому буду голосовать как минимум :-P
Крейг Уокер,

Ответы:


181

Дело не в том, что лучше, а в том, когда что использовать.

В «нормальных» случаях достаточно простого вопроса, чтобы выяснить, нужно ли нам наследование или агрегацию.

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

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

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

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

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

Давайте добавим пример. У нас есть класс «Собака» с методами: «Ешь», «Прогулка», «Кора», «Играть».

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Теперь нам нужен класс «Cat», который нуждается в «Eat», «Walk», «Purr» и «Play». Итак, сначала попробуйте расширить его от собаки.

class Cat is Dog
  Purr; 
end;

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

class Cat is Dog
  Purr; 
  Bark = null;
end;

Хорошо, это работает, но пахнет плохо. Итак, давайте попробуем агрегацию:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

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

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

Это намного чище. Нет внутренних собак. И кошки и собаки находятся на одном уровне. Мы можем даже представить других домашних животных, чтобы расширить модель. Если это не рыба или что-то, что не ходит. В этом случае нам снова нужно провести рефакторинг. Но это что-то другое время.


5
«Повторное использование почти всех функциональных возможностей класса» - это один раз, когда я действительно предпочитаю наследование. Что мне действительно нравится, так это язык, который может легко сказать «делегировать этому агрегированному объекту для этих конкретных методов»; это лучшее из обоих миров.
Крейг Уокер,

Я не уверен насчет других языков, но в Delphi есть механизм, позволяющий участникам реализовывать часть интерфейса.
Toon Krijthe

2
Цель C использует протоколы, которые позволяют вам решить, является ли ваша функция обязательной или необязательной.
cantfindaname88

1
Отличный ответ с практическим примером. Я разработал бы класс Pet с вызываемым методом makeSoundи позволил бы каждому подклассу реализовывать свой собственный тип звука. Это тогда поможет в ситуациях, когда у вас много домашних животных и вы просто делаете это for_each pet in the list of pets, pet.makeSound.
blongho


27

Разница обычно выражается как разница между «есть» и «имеет». Наследование, отношение "является", хорошо обобщено в принципе замещения Лискова . Агрегация, отношение «имеет», просто показывает, что у агрегирующего объекта есть один из агрегированных объектов.

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


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

Мне не нравится этот подход, это больше о композиции
Алиреза Рахмани Халили

15

Вот мой самый распространенный аргумент:

В любой объектно-ориентированной системе любой класс состоит из двух частей:

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

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

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

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

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

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


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

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

Как вы думаете, агрегация лучше в этом сценарии? Книга является SellingItem, а DigitalDisc является SelliingItem codereview.stackexchange.com/questions/14077/…
LCJ

11

Я дал ответ «Есть» против «Имеет»: какой из них лучше? ,

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

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

Это убедительные доказательства того, что агрегация является лучшим выбором в этом случае.


6

Я вижу много ответов «есть против», они концептуально отличаются »по этому и другим вопросам.

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

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

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

Итак, у вас есть идеальный шторм аргументов против наследования, когда вам нужно выбрать один или другой:

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

Таким образом, я склонен выбирать агрегацию - даже когда есть сильные отношения.


Развивающиеся требования означают, что ваш ОО-проект неизбежно будет ошибочным. Наследование против агрегации - это только вершина этого айсберга. Вы не можете проектировать все возможные варианты будущего, поэтому просто придерживайтесь идеи XP: решите сегодняшние требования и примите, что вам, возможно, придется провести рефакторинг.
Билл Карвин

Мне нравится эта линия вопросов и вопросов. Обеспокоенный размытостью, является, имеет, и использует, хотя. Я начинаю с интерфейсов (наряду с определением реализованных абстракций, к которым я хочу интерфейсы). Дополнительный уровень косвенности неоценим. Не следуйте своему выводу агрегации.
Orcmid

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

На мой взгляд, агрегация + интерфейсы являются альтернативой наследованию / подклассам; Вы не можете сделать полиморфизм, основанный только на агрегации.
Крейг Уокер,

3

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


Правильно, вы ... забавно, что ТАК не дал этот вопрос в качестве одного из связанных вопросов.
Крейг Уокер,

2
ТАК поиск все еще отстой большой
Vinko Vrsalovic

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

1
ТАК нужна функция, чтобы свести 2 вопроса (и их ответы) в 1
Крейг Уокер,

3

Я хотел сделать это комментарием к исходному вопросу, но укусил 300 символов [; <).

Я думаю, что мы должны быть осторожны. Во-первых, есть больше ароматов, чем два довольно конкретных примера, приведенных в вопросе.

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

Например, чего вы хотите достичь, с чего вы можете начать, и каковы ограничения?

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

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

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


2

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

Наследование здесь? Есть морской, зеленый берет и снайпер. Это типы солдат. Итак, есть солдат базового класса с морскими, зелеными беретами и снайперами в качестве производных классов

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


2

Я думаю, что это не дискуссия. Это просто так:

  1. Отношения is-a (наследование) встречаются реже, чем отношения has-a (составление).
  2. Наследование сложнее понять, даже когда оно уместно, поэтому нужно проявлять должную осмотрительность, потому что оно может нарушить инкапсуляцию, стимулировать тесную связь, выставляя реализацию и так далее.

Оба имеют свое место, но наследование рискованнее.

Хотя, конечно, не имеет смысла иметь класс Shape, имеющий классы Point и Square. Здесь наследство обусловлено.

Люди склонны думать о наследовании в первую очередь, когда пытаются создать что-то расширяемое, вот что не так.


1

Благосклонность случается, когда оба кандидата готовятся. A и B - варианты, и вы предпочитаете A. Причина в том, что композиция предлагает больше возможностей расширения / гибкости, чем обобщения. Это расширение / гибкость относится главным образом к динамической гибкости.

Благо не сразу видно. Чтобы увидеть выгоду, вам нужно дождаться следующего неожиданного запроса на изменение. Таким образом, в большинстве случаев те, кто придерживается обобщений, терпят неудачу по сравнению с теми, кто принял композицию (за исключением одного очевидного случая, упомянутого позже). Отсюда и правило. С точки зрения обучения, если вы можете успешно внедрить внедрение зависимостей, вы должны знать, какой из них предпочтительнее и когда. Правило также помогает вам принять решение; если вы не уверены, выберите композицию.

Описание: Состав: связь уменьшается благодаря наличию нескольких более мелких вещей, которые вы подключаете к чему-то большему, а более крупный объект просто вызывает меньший объект обратно. Обобщение: с точки зрения API определение, что метод может быть переопределен, является более строгим обязательством, чем определение, что метод может быть вызван. (очень мало случаев, когда Генерализация побеждает). И никогда не забывайте, что с композицией вы также используете наследование от интерфейса вместо большого класса


0

Оба подхода используются для решения разных проблем. Вам не всегда нужно объединять более двух или более классов при наследовании от одного класса.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.