Есть две школы мысли о том, как лучше всего расширить, улучшить и повторно использовать код в объектно-ориентированной системе:
Наследование: расширьте функциональность класса, создав подкласс. Переопределите члены суперкласса в подклассах, чтобы обеспечить новую функциональность. Сделайте методы абстрактными / виртуальными, чтобы подклассы «заполняли пробелы», когда суперклассу нужен конкретный интерфейс, но он не зависит от его реализации.
Агрегация: создайте новую функциональность, взяв другие классы и объединяя их в новый класс. Присоедините общий интерфейс к этому новому классу для взаимодействия с другим кодом.
Каковы преимущества, затраты и последствия каждого из них? Есть ли другие альтернативы?
Я вижу, что эти дебаты возникают на регулярной основе, но я не думаю, что их спрашивают Переполнения стека пока нет (хотя есть связанное с этим обсуждение). Также удивительно отсутствие хороших результатов в Google.


Разница обычно выражается как разница между «есть» и «имеет». Наследование, отношение «есть», красиво резюмируется в Принцип замены Лискова. Агрегация, отношение «имеет», является именно такой - она показывает, что агрегирующий объект имеет является одним из агрегированных объектов.
Существуют и другие различия - частное наследование в C++ указывает на связь «реализовано в терминах», которая также может быть смоделирована агрегацией (неоткрытых) объектов-членов.
Кроме того, разница хорошо выражена в том самом вопросе, на который вы должны ответить. Я бы хотел, чтобы люди сначала читали вопросы, прежде чем приступить к переводу. Вы вслепую продвигаете шаблоны дизайна, чтобы стать членом элитного клуба?
Мне такой подход не нравится, это скорее композиция
Дело не в том, что лучше, а в том, когда что использовать.
В «нормальных» случаях достаточно простого вопроса, чтобы выяснить, нужно ли нам наследование или агрегация.
Однако есть большая серая зона. Так что нам понадобится еще несколько уловок.
Короче. Мы должны использовать агрегацию, если часть интерфейса не используется или должна быть изменена, чтобы избежать нелогичной ситуации. Нам нужно использовать наследование только в том случае, если нам нужна почти вся функциональность без серьезных изменений. А если сомневаетесь, используйте агрегирование.
Другая возможность для случая, когда у нас есть класс, которому требуется часть функциональности исходного класса, состоит в том, чтобы разделить исходный класс на корневой класс и подкласс. И пусть новый класс наследуется от корневого класса. Но с этим следует позаботиться, чтобы не создать нелогичного разделения.
Добавим пример. У нас есть класс 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;
Это намного чище. Никаких внутренних собак. И кошки, и собаки на одном уровне. Мы даже можем представить других домашних животных, чтобы расширить модель. Если только это не рыба или что-то, что не ходит. В этом случае нам снова нужно провести рефакторинг. Но об этом в другой раз.
«Повторное использование почти всех функций класса» - это единственный раз, когда я действительно предпочитаю наследование. Я бы предпочел В самом деле язык, который может легко сказать «делегировать этому агрегированному объекту для этих конкретных методов»; это лучшее из обоих миров.
Я не уверен насчет других языков, но у Delphi есть механизм, который позволяет участникам реализовывать часть интерфейса.
Objective C использует протоколы, которые позволяют вам решать, является ли ваша функция обязательной или необязательной.
Отличный ответ с практическим примером. Я бы спроектировал класс Pet с помощью метода makeSound и позволил бы каждому подклассу реализовывать свой собственный тип звука. Это поможет в ситуациях, когда у вас много домашних животных и вы просто делаете for_each pet in the list of pets, pet.makeSound.
Я расскажу о том, где они могут применяться. Вот пример того и другого в сценарии игры. Предположим, есть игра, в которой есть разные типы солдат. У каждого солдата может быть рюкзак, в который можно положить разные вещи.
Наследование здесь? Там есть морпех, зеленый берет и снайпер. Это типы солдат. Итак, есть базовый класс Soldier с классами Marine, Green Beret и Sniper в качестве производных.
Агрегация здесь? Рюкзак может содержать гранаты, пистолеты (разных типов), нож, аптечку и т. д. Солдат может быть экипирован любым из них в любой момент времени, плюс он также может иметь бронежилет, который действует как броня при атаке, и его травма уменьшается до определенного процента. Класс солдат содержит объект класса бронежилета и класс ранца, который содержит ссылки на эти предметы.
Оба подхода используются для решения разных задач. Вам не всегда нужно объединять два или более классов при наследовании от одного класса.
Иногда вам нужно объединить один класс, потому что этот класс запечатан или имеет невиртуальные члены, которые вам нужно перехватить, поэтому вы создаете прокси-уровень, который, очевидно, недействителен с точки зрения наследования, но до тех пор, пока класс, который вы проксируете имеет интерфейс, на который вы можете подписаться, это может сработать довольно хорошо.
Я думаю, что это не дискуссия либо / либо. Просто так:
У обоих есть свое место, но наследование более рискованно.
Хотя, конечно, не имело бы смысла иметь классы Shape, которые «имеют» Point, и классы Square. Здесь полагается наследство.
Люди склонны думать в первую очередь о наследовании, когда пытаются спроектировать что-то расширяемое, вот что не так.
Вопрос обычно формулируется как Состав против наследования, и здесь он уже задавался.
Вот мой самый распространенный аргумент:
В любой объектно-ориентированной системе любой класс состоит из двух частей:
Его интерфейс: «публичное лицо» объекта. Это набор возможностей, которые он объявляет остальному миру. Во многих языках набор четко определен как «класс». Обычно это сигнатуры метода объекта, хотя это немного зависит от языка.
Его выполнение: «закулисная» работа, которую объект выполняет для удовлетворения своего интерфейса и обеспечения функциональности. Обычно это код и данные члена объекта.
Один из фундаментальных принципов ООП - это реализация инкапсулированный (т. Е. Скрытая) внутри класса; единственное, что должны видеть посторонние, - это интерфейс.
Когда подкласс наследуется от подкласса, он обычно наследует и то и другое реализацию и интерфейс. Это, в свою очередь, означает, что вы принужденный принимаете оба как ограничения вашего класса.
При агрегации вы можете выбрать либо реализацию, либо интерфейс, либо и то, и другое, но вы не обязаны делать это ни за что. Функциональность объекта оставлена на усмотрение самого объекта. Он может подчиняться другим объектам по своему усмотрению, но в конечном итоге несет ответственность за себя. По моему опыту, это приводит к более гибкой системе: ее легче модифицировать.
Итак, когда я разрабатываю объектно-ориентированное программное обеспечение, я почти всегда предпочитаю агрегирование наследованию.
Я бы подумал, что вы используете абстрактный класс для определения интерфейса, а затем используете прямое наследование для каждого конкретного класса реализации (что делает его довольно мелким). Любая агрегация находится под конкретной реализацией.
Если вам нужны дополнительные интерфейсы, верните их с помощью методов интерфейса абстрактного интерфейса основного уровня, промойте и повторите.
Как вы думаете, в этом сценарии лучше агрегация? Книга - это SellingItem, DigitalDisc - SelliingItem codereview.stackexchange.com/questions/14077/…
Я дал ответ «Есть» или «Имеет»: что лучше?.
В основном я согласен с другими людьми: используйте наследование только в том случае, если ваш производный класс действительно является тип, который вы расширяете, а не только потому, что он содержит те же данные. Помните, что наследование означает, что подкласс получает не только данные, но и методы.
Имеет ли смысл для вашего производного класса иметь все методы суперкласса? Или вы просто тихо пообещаете себе, что эти методы следует игнорировать в производном классе? Или вы обнаруживаете, что переопределяете методы из суперкласса, делая их закрытыми, чтобы никто не вызвал их случайно? Или дать подсказку вашему инструменту создания документации API, чтобы исключить метод из документа?
Это веские доказательства того, что в этом случае лучше использовать агрегацию.
Я вижу много ответов на этот и связанные с ним вопросы типа «есть-а-а-а-а; они концептуально разные».
Единственное, что я обнаружил на своем опыте, это то, что попытка определить, являются ли отношения отношениями «есть-а» или «имеет-а», обречена на провал. Даже если вы можете правильно сделать это определение для объектов сейчас, изменение требований означает, что вы, вероятно, ошибетесь в какой-то момент в будущем.
Еще я обнаружил, что очень трудно преобразовать из наследования в агрегацию, если вокруг иерархии наследования написано много кода. Простое переключение с суперкласса на интерфейс означает изменение почти каждого подкласса в системе.
И, как я упоминал в другом месте в этом посте, агрегирование имеет тенденцию быть менее гибким, чем наследование.
Итак, у вас есть идеальный шторм аргументов против наследования всякий раз, когда вам нужно выбрать одно или другое:
Таким образом, я склонен выбирать агрегацию - даже когда кажется, что существует сильная связь.
Растущие требования означают, что ваш объектно-ориентированный дизайн неизбежно будет неправильным. Наследование и агрегация - это лишь верхушка айсберга. Вы не можете спроектировать все возможные варианты будущего, поэтому просто следуйте идее XP: выполните сегодняшние требования и примите тот факт, что вам, возможно, придется провести рефакторинг.
Мне нравится эта линия расследования и вопрошания. Обеспокоен тем, что blurrung is-a, has-a и uses-a. Я начинаю с интерфейсов (наряду с определением реализованных абстракций, к которым мне нужны интерфейсы). Дополнительный уровень косвенности бесценен. Не следуйте своему заключению по агрегированию.
Хотя это в значительной степени моя точка зрения. И наследование, и агрегация - это методы для решения проблемы. Один из этих методов влечет за собой всевозможные штрафы, если вам нужно решать проблемы завтра.
На мой взгляд, агрегация + интерфейсы - альтернатива наследованию / подклассу; вы не можете сделать полиморфизм, основанный только на агрегации.
Я хотел сделать это комментарием к исходному вопросу, но 300 символов кусаются [; <).
Я думаю, нам нужно быть осторожными. Во-первых, вкусов больше, чем двух довольно конкретных примеров, приведенных в вопросе.
Кроме того, я предлагаю не путать цель с инструментом. Кто-то хочет быть уверенным, что выбранная техника или методология поддерживает достижение основной цели, но я не считаю, что обсуждение того, какая техника лучше, вне контекста, очень полезно. Это действительно помогает узнать о подводных камнях различных подходов, а также их четкие зоны наилучшего воздействия.
Например, чего вы хотите достичь, что у вас есть для начала и каковы ограничения?
Вы создаете компонентную структуру, даже специальную? Отделены ли интерфейсы от реализаций в системе программирования или это достигается практикой с использованием другой технологии? Можете ли вы отделить структуру наследования интерфейсов (если есть) от структуры наследования классов, которые их реализуют? Важно ли скрыть структуру классов реализации от кода, который полагается на интерфейсы, предоставляемые реализацией? Есть ли несколько реализаций, которые можно использовать одновременно, или изменение происходит с течением времени в результате обслуживания и улучшения? Это и многое другое необходимо учитывать, прежде чем вы остановитесь на каком-либо инструменте или методологии.
Наконец, так ли важно закрепить различия в абстракции и в том, как вы относитесь к ней (например, is-a по сравнению с has-a), к различным функциям объектно-ориентированной технологии? Возможно, так, если он сохраняет концептуальную структуру последовательной и управляемой для вас и других. Но мудро не быть порабощенным этим и теми искажениями, которые вы можете в конечном итоге сделать. Может быть, лучше отступить на уровень и не быть таким жестким (но оставить хорошее повествование, чтобы другие могли сказать, что происходит). [Я ищу, что делает конкретную часть программы объяснимой, но иногда я прибегаю к элегантности, когда есть более крупный выигрыш. Не всегда лучшая идея.]
Я сторонник интерфейсов, и меня привлекают проблемы и подходы, в которых уместен пуризм интерфейсов, будь то создание инфраструктуры Java или организация некоторых реализаций COM. Это не делает его подходящим для всего, даже близко ко всему, хотя я клянусь этим. (У меня есть пара проектов, которые, кажется, предоставляют серьезные контрпримеры против пуризма интерфейсов, поэтому будет интересно посмотреть, как мне удастся справиться с этим.)
Благосклонность случается, когда оба кандидата соответствуют требованиям. A и B - варианты, и вы предпочитаете A. Причина в том, что композиция предлагает больше возможностей расширения / гибкости, чем обобщение. Это расширение / гибкость в основном относится к гибкости времени выполнения / динамической гибкости.
Выгода не сразу видна. Чтобы увидеть преимущество, вам нужно дождаться следующего неожиданного запроса на изменение. Так что в большинстве случаев те, кто придерживался обобщения, терпят неудачу по сравнению с теми, кто принял композицию (за исключением одного очевидного случая, упомянутого позже). Отсюда правило. С точки зрения обучения, если вы можете успешно реализовать внедрение зависимостей, вы должны знать, какой из них отдать предпочтение и когда. Правило также помогает вам принять решение; если вы не уверены, выберите композицию.
Резюме: Композиция: связь уменьшается, если вы подключаете несколько более мелких вещей к чему-то большему, а более крупный объект просто вызывает меньший объект. Обобщение: с точки зрения API определение возможности переопределения метода является более сильным обязательством, чем определение возможности вызова метода. (очень мало случаев, когда побеждает обобщение). И никогда не забывайте, что с композицией вы также используете наследование от интерфейса вместо большого класса.
Связанный: Предпочитаете композицию наследованию?