Я всегда читаю, что композиция должна быть предпочтительнее наследования. Например, пост в блоге, посвященный разным видам , пропагандирует использование композиции вместо наследования, но я не вижу, как достигается полиморфизм.
Но у меня есть ощущение, что когда люди говорят, что они предпочитают композицию, они действительно подразумевают сочетание композиции и реализации интерфейса. Как вы собираетесь получить полиморфизм без наследования?
Вот конкретный пример, где я использую наследование. Как это изменить, чтобы использовать композицию, и что я получу?
Class Shape
{
string name;
public:
void getName();
virtual void draw()=0;
}
Class Circle: public Shape
{
void draw(/*draw circle*/);
}
interfaces
inheritance
composition
MustafaM
источник
источник
Ответы:
Полиморфизм не обязательно подразумевает наследование. Часто наследование используется как простое средство для реализации полиморфного поведения, потому что удобно классифицировать похожие объекты поведения как имеющие полностью общую корневую структуру и поведение. Вспомните все те примеры кодов автомобилей и собак, которые вы видели за эти годы.
Но как насчет объектов, которые не совпадают. Моделирование автомобиля и планеты будет очень разным, и все же оба могут захотеть реализовать поведение Move ().
На самом деле, вы в основном ответили на свой вопрос, когда сказали
"But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation."
. Общее поведение может быть обеспечено через интерфейсы и поведенческую композицию.Что касается того, что лучше, то ответ несколько субъективен и в действительности сводится к тому, как вы хотите, чтобы ваша система работала, что имеет смысл как в контекстном, так и в архитектурном отношении, и насколько легко будет тестировать и поддерживать.
источник
Предпочтительная композиция - это не только полиморфизм. Хотя это является частью этого, и вы правы в том, что (по крайней мере, в номинально типизированных языках) люди на самом деле имеют в виду «предпочесть сочетание композиции и реализации интерфейса». Но причины для предпочтения композиции (во многих обстоятельствах) глубокие.
Полиморфизм - это то, что ведет себя по-разному. Таким образом, дженерики / шаблоны являются «полиморфной» функцией, поскольку они позволяют одному куску кода изменять свое поведение в зависимости от типов. Фактически, этот тип полиморфизма действительно лучше всего ведет себя и обычно упоминается как параметрический полиморфизм, потому что изменение определяется параметром.
Многие языки предоставляют форму полиморфизма, называемую «перегрузкой» или специальным полиморфизмом, когда несколько процедур с одним и тем же именем определяются специальным образом, и когда одна из них выбирается языком (возможно, наиболее конкретным). Это наименее хорошо управляемый вид полиморфизма, поскольку ничто не связывает поведение двух процедур, кроме разработанного соглашения.
Третий тип полиморфизма - это полиморфизм подтипа . Здесь процедура, определенная для данного типа, также может работать с целым семейством «подтипов» этого типа. Когда вы реализуете интерфейс или расширяете класс, вы, как правило, заявляете о своем намерении создать подтип. Истинные подтипы подчиняются принципу замещения ЛисковаЭто говорит о том, что если вы можете доказать что-то обо всех объектах в супертипе, вы можете доказать это обо всех экземплярах в подтипе. Однако жизнь становится опасной, поскольку в таких языках, как C ++ и Java, люди обычно имеют необоснованные и часто недокументированные предположения о классах, которые могут быть верны или не соответствовать их подклассам. То есть код написан так, как будто больше доказуемости, чем на самом деле, что создает целый ряд проблем, когда вы небрежно вводите подтипы.
Наследование на самом деле не зависит от полиморфизма. Учитывая некоторую вещь "T", которая имеет ссылку на себя, наследование происходит, когда вы создаете новую вещь "S" из "T", заменяя ссылку "T" на себя ссылкой на "S". Это определение намеренно расплывчато, поскольку наследование может происходить во многих ситуациях, но наиболее распространенным является создание подкласса объекта, который заменяет
this
указатель, вызываемый виртуальными функциями,this
указателем на подтип.Наследование является опасным, как и все очень мощные вещи, наследование может вызвать хаос. Например, предположим, что вы переопределяете метод при наследовании от некоторого класса: все хорошо, пока какой-то другой метод этого класса не примет метод, который вы наследуете, чтобы вести себя определенным образом, ведь именно так его разработал автор исходного класса. , Вы можете частично защититься от этого, объявив все методы, вызываемые другим из ваших методов, закрытыми или не виртуальными (финальными), если только они не предназначены для переопределения. Даже это не всегда достаточно хорошо. Иногда вы можете увидеть что-то вроде этого (в псевдо-Java, надеюсь, читабельно для пользователей C ++ и C #)
вы думаете, что это прекрасно, и у вас есть свой собственный способ делать "вещи", но вы используете наследование, чтобы приобрести способность делать "больше вещей",
И все хорошо.
WayOfDoingUsefulThings
был разработан таким образом, что замена одного метода не меняет семантику любого другого ... за исключением ожидания, нет, это не так. Похоже, что это было, ноdoThings
изменилось изменчивое состояние, которое имело значение. Таким образом, даже при том, что он не вызывал никаких переопределяемых функций,теперь делает что-то другое, чем ожидалось, когда вы передаете это
MyUsefulThings
. Что еще хуже, вы можете даже не знать, чтоWayOfDoingUsefulThings
дали эти обещания. Может быть,dealWithStuff
поступает из той же библиотеки, чтоWayOfDoingUsefulThings
иgetStuff()
даже не экспортируется библиотекой (вспомните о классах друзей в C ++). Что еще хуже, вы победили статические проверки языка , не осознавая этого:dealWithStuff
взялWayOfDoingUsefulThings
только , чтобы убедиться , что она будет иметьgetStuff()
функцию , которая вела себя определенным образом.Использование композиции
возвращает безопасность статического типа. В общем случае композиция проще в использовании и безопаснее наследования при реализации подтипов. Это также позволяет вам переопределить final методы, что означает, что вы можете свободно объявлять все final / non-virtual за исключением интерфейсов, которые подавляющее большинство времени.
В лучшем мире языки будут автоматически вставлять шаблон с
delegation
ключевым словом. Большинство этого не делает, поэтому недостатком являются большие классы. Хотя вы можете заставить свою IDE написать делегирующий экземпляр для вас.Теперь жизнь не только о полиморфизме. Вам не нужно постоянно вводить подтипы. Целью полиморфизма обычно является повторное использование кода, но это не единственный способ достижения этой цели. Часто имеет смысл использовать композицию без полиморфизма подтипа в качестве способа управления функциональностью.
Кроме того, поведенческое наследование имеет свое применение. Это одна из самых сильных идей в информатике. Просто в большинстве случаев хорошие ООП-приложения могут быть написаны с использованием только наследования интерфейса и композиций. Два принципа
являются хорошим руководством по вышеуказанным причинам и не несут существенных затрат.
источник
Причина, по которой люди так говорят, заключается в том, что начинающие программисты ООП, только что начавшие свои лекции по полиморфизму через наследование, как правило, пишут большие классы с множеством полиморфных методов, а затем где-то в конце они оказываются в неразрешимом беспорядке.
Типичным примером является мир разработки игр. Предположим, у вас есть базовый класс для всех ваших игровых объектов - космического корабля игрока, монстров, пуль и т. Д .; каждый тип сущности имеет свой собственный подкласс. Подход наследования будет использовать несколько полиморфных методов, например
update_controls()
,update_physics()
,draw()
и т.д., и реализовать их для каждого подкласса. Однако это означает, что вы соединяете несвязанную функциональность: не имеет значения, как выглядит объект для его перемещения, и вам не нужно ничего знать о его ИИ, чтобы нарисовать его. Вместо этого композиционный подход определяет несколько базовых классов (или интерфейсов), напримерEntityBrain
(подклассы реализуют ввод AI или игрока),EntityPhysics
(подклассы реализуют физику движения) иEntityPainter
(подклассы заботятся о рисовании) и неполиморфный классEntity
который содержит один экземпляр каждого. Таким образом, вы можете комбинировать любой внешний вид с любой физической моделью и любым ИИ, и, поскольку вы держите их отдельно, ваш код также будет намного чище. Кроме того, проблемы, такие как «Я хочу монстра, который похож на монстра-шарика на уровне 1, но ведет себя как сумасшедший клоун на уровне 15» исчезают: вы просто берете подходящие компоненты и склеиваете их вместе.Обратите внимание, что композиционный подход все еще использует наследование внутри каждого компонента; хотя в идеале вы бы использовали только интерфейсы и их реализации здесь.
«Разделение интересов» - это ключевая фраза: представление физики, реализация ИИ и рисование объекта - это три задачи, объединение их в объект - это четвертое. При композиционном подходе каждая проблема моделируется как один класс.
источник
MonsterVisuals balloonVisuals
иMonsterBehaviour crazyClownBehaviour
и экземплярMonster(balloonVisuals, crazyClownBehaviour)
, наряду сMonster(balloonVisuals, balloonBehaviour)
иMonster(crazyClownVisuals, crazyClownBehaviour)
экземплярами, экземпляры которых были на уровне 1 и 15 уровняВы привели пример, в котором наследование является естественным выбором. Я не думаю, что кто-то может утверждать, что композиция всегда является лучшим выбором, чем наследование - это всего лишь руководство, которое означает, что зачастую лучше собрать несколько относительно простых объектов, чем создавать множество узкоспециализированных объектов.
Делегирование является одним из примеров использования композиции вместо наследования. Делегирование позволяет вам изменять поведение класса без создания подклассов. Рассмотрим класс, который обеспечивает сетевое соединение, NetStream. Подкласс NetStream может быть естественным для реализации общего сетевого протокола, поэтому вы можете использовать FTPStream и HTTPStream. Но вместо создания очень специфического подкласса HTTPStream для одной цели, скажем, UpdateMyWebServiceHTTPStream, часто лучше использовать простой старый экземпляр HTTPStream вместе с делегатом, который знает, что делать с данными, которые он получает от этого объекта. Одна из причин, почему это лучше, - это то, что он избегает распространения классов, которые необходимо поддерживать, но которые вы никогда не сможете использовать повторно.
источник
Вы много увидите этот цикл в дискурсе разработки программного обеспечения:
Обнаружено, что некоторая функция или шаблон (назовем его «Шаблон X») полезна для какой-то конкретной цели. Сообщения в блоге написаны, превознося достоинства Pattern X.
Шумиха заставляет некоторых людей думать, что вы должны использовать Pattern X, когда это возможно .
Другие люди раздражаются, увидев, что Pattern X используется в тех случаях, когда он не подходит, и они пишут в блоге сообщения о том, что вы не всегда должны использовать Pattern X и что это вредно в некоторых случаях.
Обратная реакция заставляет некоторых людей полагать, что Шаблон X всегда вреден и никогда не должен использоваться.
Вы увидите, что этот цикл шумихи / обратной реакции происходит практически с любой функцией, от
GOTO
шаблонов до SQL, NoSQL и, да, наследования. Противоядием является всегда учитывать контекст .После
Circle
спускаются отShape
это точно , как предполагается наследование для использования в объектно- ориентированных языках , поддерживающих наследование.Практическое правило «предпочесть композицию наследованию» действительно вводит в заблуждение без контекста. Вы должны предпочитать наследование, когда наследование более уместно, но предпочитать композицию, когда композиция более уместна. Предложение предназначено для людей на стадии 2 в цикле рекламы, которые думают, что наследование должно использоваться везде. Но цикл продолжился, и сегодня кажется, что некоторые люди думают, что наследование как-то плохо само по себе.
Думайте об этом как молоток против отвертки. Стоит ли предпочитать отвертку молотку? Вопрос не имеет смысла. Вы должны использовать инструмент, подходящий для работы, и все зависит от того, какую задачу вам нужно выполнить.
источник