Наследование против агрегации

Есть две школы мысли о том, как лучше всего расширить, расширить и повторно использовать код в объектно-ориентированной системе:

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

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

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

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

Ответ 1

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

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

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

Однако есть большая серая область. Поэтому нам нужны еще несколько трюков.

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

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

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

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

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

Теперь нам нужен класс "Cat", которому нужны "Eat", "Walk", "Purr" и "Play". Поэтому сначала попробуйте расширить его от Собаки.

class Cat is Dog
  Purr; 
end;

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

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;

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

Ответ 2

В начале GOF они указывают

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

Далее рассматривается здесь

Ответ 3

Разница обычно выражается как разница между "is a" и "has a". Наследование, "является" отношением, прекрасно подводится в Принципе замещения Лискова. Агрегация, "имеет" отношение, является именно этим - она ​​показывает, что агрегирующий объект имеет один из агрегированных объектов.

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

Ответ 4

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

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

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

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

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

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

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

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

Ответ 5

Я дал ответ "Есть ли" vs "Have a" : какой из них лучше?.

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

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

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

Ответ 6

Я вижу много ответов "это-а-а-а-а, они концептуально разные" на этот и связанные вопросы.

Единственное, что я нашел в своем опыте, состоит в том, что попытка определить, является ли связь "is-a" или "has-a", обязана сбой. Даже если вы можете правильно сделать это определение для объектов сейчас, изменение требований означает, что вы, вероятно, ошибаетесь в какой-то момент в будущем.

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

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

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

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

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

Ответ 8

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

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

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

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

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

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

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

Ответ 9

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

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

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

Хотя, конечно, не имеет смысла иметь класс Shape, имеющий "a" и "Square". Здесь наследование происходит.

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

Ответ 10

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

Наследование здесь? Там морской, зеленый берет и снайпер. Это типы солдат. Итак, есть базовый класс Soldier с Marine, Green Beret и Sniper в качестве производных классов

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

Ответ 11

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

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

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

Ответ 12

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

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