Существует два подхода к расширению, расширению и повторному использованию кода в объектно-ориентированной системе:
Наследование: расширить функциональность класса, создав подкласс. Переопределите члены суперкласса в подклассах, чтобы обеспечить новую функциональность. Сделайте методы абстрактными / виртуальными, чтобы заставить подклассы «заполнять пробелы», когда суперклассу нужен конкретный интерфейс, но он не зависит от его реализации.
Агрегация: создайте новую функциональность, беря другие классы и объединяя их в новый класс. Присоедините общий интерфейс к этому новому классу для взаимодействия с другим кодом.
Каковы преимущества, затраты и последствия каждого? Есть ли другие альтернативы?
Я вижу, что эти дебаты возникают регулярно, но я не думаю, что их уже спрашивали о переполнении стека (хотя есть некоторые связанные обсуждения). Там также удивительное отсутствие хороших результатов Google для этого.
источник
Ответы:
Дело не в том, что лучше, а в том, когда что использовать.
В «нормальных» случаях достаточно простого вопроса, чтобы выяснить, нужно ли нам наследование или агрегацию.
Тем не менее, есть большая серая зона. Итак, нам нужно несколько других трюков.
Короче говоря. Мы должны использовать агрегацию, если часть интерфейса не используется или должна быть изменена, чтобы избежать нелогичной ситуации. Нам нужно только использовать наследование, если нам нужны почти все функциональные возможности без серьезных изменений. И если есть сомнения, используйте агрегацию.
Другая возможность для случая, когда у нас есть класс, которому требуется часть функциональности исходного класса, - это разделить исходный класс на корневой класс и подкласс. И пусть новый класс наследуется от корневого класса. Но вы должны позаботиться об этом, чтобы не создавать нелогичное разделение.
Давайте добавим пример. У нас есть класс «Собака» с методами: «Ешь», «Прогулка», «Кора», «Играть».
Теперь нам нужен класс «Cat», который нуждается в «Eat», «Walk», «Purr» и «Play». Итак, сначала попробуйте расширить его от собаки.
Выглядит хорошо, но подожди. Эта кошка может лаять (меня за это убьют любители кошек). А лающий кот нарушает принципы вселенной. Поэтому нам нужно переопределить метод Барка, чтобы он ничего не делал.
Хорошо, это работает, но пахнет плохо. Итак, давайте попробуем агрегацию:
Хорошо это хорошо Этот кот больше не лает, даже не молчит. Но все же у него есть внутренняя собака, которая хочет выйти. Итак, давайте попробуем решение номер три:
Это намного чище. Нет внутренних собак. И кошки и собаки находятся на одном уровне. Мы можем даже представить других домашних животных, чтобы расширить модель. Если это не рыба или что-то, что не ходит. В этом случае нам снова нужно провести рефакторинг. Но это что-то другое время.
источник
makeSound
и позволил бы каждому подклассу реализовывать свой собственный тип звука. Это тогда поможет в ситуациях, когда у вас много домашних животных и вы просто делаете этоfor_each pet in the list of pets, pet.makeSound
.В начале GOF они заявляют
Это дополнительно обсуждается здесь
источник
Разница обычно выражается как разница между «есть» и «имеет». Наследование, отношение "является", хорошо обобщено в принципе замещения Лискова . Агрегация, отношение «имеет», просто показывает, что у агрегирующего объекта есть один из агрегированных объектов.
Существуют и другие различия - частное наследование в C ++ указывает на то, что отношение «реализовано в терминах», которое также может быть смоделировано с помощью агрегации (незащищенных) объектов-членов.
источник
Вот мой самый распространенный аргумент:
В любой объектно-ориентированной системе любой класс состоит из двух частей:
Его интерфейс : «публичное лицо» объекта. Это набор возможностей, которые он объявляет остальному миру. Во многих языках набор четко определен как «класс». Обычно это сигнатуры методов объекта, хотя они немного различаются в зависимости от языка.
Его реализация : «закулисная» работа, которую объект выполняет, чтобы удовлетворить свой интерфейс и обеспечить функциональность. Обычно это код и данные члена объекта.
Один из фундаментальных принципов ООП заключается в том, что реализация инкапсулирована (т.е. скрыта) внутри класса; единственное, что должны видеть посторонние - это интерфейс.
Когда подкласс наследует от подкласса, он обычно наследует и реализацию, и интерфейс. Это, в свою очередь, означает, что вы вынуждены принять оба ограничения в своем классе.
При агрегации вы можете выбрать либо реализацию, либо интерфейс, либо и то и другое, но вы не обязаны это делать. Функциональность объекта оставлена на усмотрение самого объекта. Он может подчиняться другим объектам так, как ему нравится, но в конечном итоге он отвечает за себя. По моему опыту, это приводит к более гибкой системе, которую легче модифицировать.
Поэтому, когда я занимаюсь разработкой объектно-ориентированного программного обеспечения, я почти всегда предпочитаю агрегацию, а не наследование.
источник
Я дал ответ «Есть» против «Имеет»: какой из них лучше? ,
По сути, я согласен с другими людьми: используйте наследование, только если ваш производный класс действительно является типом, который вы расширяете, а не просто потому, что он содержит те же данные. Помните, что наследование означает, что подкласс получает методы и данные.
Имеет ли смысл для вашего производного класса иметь все методы суперкласса? Или вы просто спокойно обещаете себе, что эти методы должны игнорироваться в производном классе? Или вы обнаружите, что вы переопределяете методы суперкласса, делая их неактивными, чтобы никто не вызывал их случайно? Или дать подсказки своему инструменту создания документации API, чтобы пропустить метод из документа?
Это убедительные доказательства того, что агрегация является лучшим выбором в этом случае.
источник
Я вижу много ответов «есть против», они концептуально отличаются »по этому и другим вопросам.
Одна вещь, которую я обнаружил в своем опыте, заключается в том, что попытка определить, является ли отношение «есть-а» или «имеет-а», обязательно потерпит неудачу. Даже если вы можете правильно сделать это определение для объектов сейчас, меняющиеся требования означают, что вы, вероятно, ошибетесь в какой-то момент в будущем.
Еще одна вещь, которую я нашел, это то, что это очень трудно преобразовать наследование в агрегацию, если вокруг иерархии наследования написано много кода. Простое переключение с суперкласса на интерфейс означает изменение почти каждого подкласса в системе.
И, как я уже упоминал в этой статье, агрегация, как правило, менее гибкая, чем наследование.
Итак, у вас есть идеальный шторм аргументов против наследования, когда вам нужно выбрать один или другой:
Таким образом, я склонен выбирать агрегацию - даже когда есть сильные отношения.
источник
Вопрос обычно формулируется как композиция против наследования , и он уже задавался здесь ранее.
источник
Я хотел сделать это комментарием к исходному вопросу, но укусил 300 символов [; <).
Я думаю, что мы должны быть осторожны. Во-первых, есть больше ароматов, чем два довольно конкретных примера, приведенных в вопросе.
Кроме того, я полагаю, что важно не путать цель с инструментом. Кто-то хочет удостовериться, что выбранная техника или методология поддерживает достижение основной цели, но я не думаю, что вне контекста обсуждение «техника - лучшая» очень полезно. Это помогает узнать подводные камни различных подходов, а также их четкие сладкие места.
Например, чего вы хотите достичь, с чего вы можете начать, и каковы ограничения?
Вы создаете компонентный каркас, даже специальный? Являются ли интерфейсы отделимыми от реализаций в системе программирования или это достигается практикой, использующей различные виды технологий? Можете ли вы отделить структуру наследования интерфейсов (если таковые имеются) от структуры наследования классов, которые их реализуют? Важно ли скрывать структуру классов реализации от кода, который опирается на интерфейсы, предоставляемые реализацией? Есть ли несколько реализаций, которые можно использовать одновременно, или это изменение становится более продолжительным в результате обслуживания и улучшения? Это и многое другое необходимо учитывать, прежде чем зацикливаться на инструменте или методологии.
Наконец, важно ли зафиксировать различия в абстракции и как вы относитесь к ней (как в «против») с различными функциями технологии ОО? Возможно, если она поддерживает концептуальную структуру последовательной и управляемой для вас и других. Но разумно не быть порабощенным этим и извращениями, которые вы могли бы в конечном итоге сделать. Может быть, лучше отойти от уровня и не быть таким жестким (но оставить хорошее повествование, чтобы другие могли сказать, что случилось). [Я ищу то, что делает конкретную часть программы объяснимой, но иногда я стремлюсь к элегантности, когда есть большая победа. Не всегда лучшая идея.]
Я пурист интерфейса, и меня привлекают проблемы и подходы, в которых уместен пуризм интерфейса, будь то создание инфраструктуры Java или организация некоторых реализаций COM. Это не делает это подходящим для всего, даже близко ко всему, хотя я клянусь этим. (У меня есть пара проектов, которые, кажется, предоставляют серьезные контрпримеры против пуризма интерфейса, поэтому будет интересно посмотреть, как мне удается справиться с этим.)
источник
Я расскажу о том, где они могут быть применены. Вот пример того и другого в игровом сценарии. Предположим, есть игра, в которой есть разные типы солдат. У каждого солдата может быть рюкзак, в котором можно хранить разные вещи.
Наследование здесь? Есть морской, зеленый берет и снайпер. Это типы солдат. Итак, есть солдат базового класса с морскими, зелеными беретами и снайперами в качестве производных классов
Агрегация здесь? Рюкзак может содержать гранаты, оружие (разных типов), нож, аптечку и т. Д. Солдат может быть экипирован любым из них в любой момент времени, плюс он также может иметь пуленепробиваемый жилет, который действует как броня при нападении, и его травма уменьшается до определенного процента. Класс солдат содержит объект класса бронежилета и класс ранца, который содержит ссылки на эти предметы.
источник
Я думаю, что это не дискуссия. Это просто так:
Оба имеют свое место, но наследование рискованнее.
Хотя, конечно, не имеет смысла иметь класс Shape, имеющий классы Point и Square. Здесь наследство обусловлено.
Люди склонны думать о наследовании в первую очередь, когда пытаются создать что-то расширяемое, вот что не так.
источник
Благосклонность случается, когда оба кандидата готовятся. A и B - варианты, и вы предпочитаете A. Причина в том, что композиция предлагает больше возможностей расширения / гибкости, чем обобщения. Это расширение / гибкость относится главным образом к динамической гибкости.
Благо не сразу видно. Чтобы увидеть выгоду, вам нужно дождаться следующего неожиданного запроса на изменение. Таким образом, в большинстве случаев те, кто придерживается обобщений, терпят неудачу по сравнению с теми, кто принял композицию (за исключением одного очевидного случая, упомянутого позже). Отсюда и правило. С точки зрения обучения, если вы можете успешно внедрить внедрение зависимостей, вы должны знать, какой из них предпочтительнее и когда. Правило также помогает вам принять решение; если вы не уверены, выберите композицию.
Описание: Состав: связь уменьшается благодаря наличию нескольких более мелких вещей, которые вы подключаете к чему-то большему, а более крупный объект просто вызывает меньший объект обратно. Обобщение: с точки зрения API определение, что метод может быть переопределен, является более строгим обязательством, чем определение, что метод может быть вызван. (очень мало случаев, когда Генерализация побеждает). И никогда не забывайте, что с композицией вы также используете наследование от интерфейса вместо большого класса
источник
Оба подхода используются для решения разных проблем. Вам не всегда нужно объединять более двух или более классов при наследовании от одного класса.
Иногда вам нужно объединить один класс, потому что этот класс запечатан или содержит не виртуальные члены, которые вам нужно перехватить, чтобы создать прокси-слой, который, очевидно, недопустим с точки зрения наследования, но при условии, что класс вы проксируете имеет интерфейс, на который вы можете подписаться, это может сработать довольно хорошо.
источник