it-roy-ru.com

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

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

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

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

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

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

136
Craig Walker

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

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

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

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

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

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

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

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

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;

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

164
Toon Krijthe

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

Воспользуйтесь композицией объектов, а не наследованием классов.

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

36
toolkit

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

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

25
Harper Shelby

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

В любой объектно-ориентированной системе есть два компонента любого класса:

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

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

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

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

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

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

14
Craig Walker

Я дал ответ "Является ли" против "Имеет": какой из них лучше? .

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

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

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

11
Bill Karwin

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

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

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

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

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

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

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

6
Craig Walker

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

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

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

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

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

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

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

3
orcmid

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

3
Bill the Lizard

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

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

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

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

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

2
Vinko Vrsalovic

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

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

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

1
Blue Clouds

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

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

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

1
Salman Kasbati

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

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

0
cfeduke