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

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

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

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

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

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

Связанный: Предпочитаете композицию наследованию?

Melebius 13.05.2020 11:00
В PHP
В PHP
В большой кодовой базе с множеством различных компонентов классы, функции и константы могут иметь одинаковые имена. Это может привести к путанице и...
Принцип подстановки Лискова
Принцип подстановки Лискова
Принцип подстановки Лискова (LSP) - это принцип объектно-ориентированного программирования, который гласит, что объекты суперкласса должны иметь...
153
1
95 448
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 12

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

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

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

Val 17.01.2013 23:19

Мне такой подход не нравится, это скорее композиция

Monsieur Aliréza 01.04.2018 15:19

В начале GOF они заявляют

Favor object composition over class inheritance.

Это обсуждается далее здесь

Ответ принят как подходящий

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

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

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

Однако есть большая серая зона. Так что нам понадобится еще несколько уловок.

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

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

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

Добавим пример. У нас есть класс Dog с методами: Eat, Walk, Bark, Play.

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

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

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;

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

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

Craig Walker 06.11.2008 22:45

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

Toon Krijthe 06.11.2008 23:14

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

cantfindaname88 20.01.2013 07:29

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

blongho 26.08.2019 08:42

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

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

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

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

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

Я думаю, что это не дискуссия либо / либо. Просто так:

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

У обоих есть свое место, но наследование более рискованно.

Хотя, конечно, не имело бы смысла иметь классы Shape, которые «имеют» Point, и классы Square. Здесь полагается наследство.

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

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

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

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

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

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

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

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

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

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

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

orcmid 07.11.2008 02:55

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

orcmid 07.11.2008 02:57

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

LCJ 01.08.2012 15:24

Я дал ответ «Есть» или «Имеет»: что лучше?.

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

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

Это веские доказательства того, что в этом случае лучше использовать агрегацию.

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

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

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

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

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

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

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

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

Bill Karwin 06.11.2008 21:19

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

orcmid 06.11.2008 22:32

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

Craig Walker 06.11.2008 22:39

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

Craig Walker 06.11.2008 22:41

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

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

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

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

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

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

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

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

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

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

Другие вопросы по теме