«Предпочитаю композицию наследованию» - это просто хорошая эвристика
Вы должны учитывать контекст, так как это не универсальное правило. Не принимайте это, чтобы означать, никогда не используйте наследование, когда вы можете сделать композицию. Если бы это было так, вы бы это исправили, запретив наследование.
Я надеюсь прояснить этот момент в этом посте.
Я не буду пытаться защищать достоинства композиции сам по себе. Который я считаю не по теме. Вместо этого я расскажу о некоторых ситуациях, когда разработчик может рассмотреть наследование, которое было бы лучше при использовании композиции. На этом примечании наследование имеет свои достоинства, которые я также считаю не по теме.
Пример автомобиля
Я пишу о разработчиках, которые стараются изо всех сил в повествовательных целях
Пойдемте вариант классических примеров , что некоторые курсы ООП использовать ... У нас есть Vehicle
класс, то мы получим Car
, Airplane
, Balloon
и Ship
.
Примечание . Если вам нужно обосновать этот пример, представьте, что это такие объекты в видеоигре.
Тогда Car
и Airplane
может иметь какой-то общий код, потому что они оба могут кататься на колесе по земле. Разработчики могут рассмотреть возможность создания промежуточного класса в цепочке наследования для этого. Тем не менее, на самом деле есть и некоторый общий код между Airplane
и Balloon
. Для этого они могут рассмотреть возможность создания другого промежуточного класса в цепочке наследования.
Таким образом, разработчик будет искать множественное наследование. В тот момент, когда разработчики ищут множественное наследование, дизайн уже ошибся.
Лучше моделировать это поведение как интерфейсы и композицию, поэтому мы можем использовать его повторно, не сталкиваясь с наследованием нескольких классов. Если разработчики, например, создают FlyingVehicule
класс. Они будут говорить, что Airplane
это FlyingVehicule
(наследование классов), но мы могли бы вместо этого сказать, что оно Airplane
имеет Flying
компонент (состав) и Airplane
является IFlyingVehicule
(наследование интерфейса).
Используя интерфейсы, при необходимости мы можем иметь множественное наследование (интерфейсов). Кроме того, вы не привязаны к конкретной реализации. Повышение возможности повторного использования и тестирования вашего кода.
Помните, что наследование является инструментом полиморфизма. Кроме того, полиморфизм является инструментом для повторного использования. Если вы можете увеличить возможность повторного использования вашего кода с помощью композиции, то сделайте это. Если вы не уверены, какая композиция или нет обеспечивает лучшую возможность повторного использования, «Предпочитать композицию перед наследованием» будет хорошей эвристикой.
Все это без упоминания Amphibious
.
На самом деле, нам могут не понадобиться вещи, которые оторвутся от земли. У Стивена Херна есть более красноречивый пример в его статьях «Композиция в пользу наследования», часть 1 и часть 2 .
Заменимость и инкапсуляция
Должны A
наследовать или сочинять B
?
Если A
это специализация, B
которая должна соответствовать принципу подстановки Лискова , наследование жизнеспособно, даже желательно. Если есть ситуации, когда A
замена B
недопустима, мы не должны использовать наследование.
Возможно, нас заинтересует композиция как форма защитного программирования для защиты производного класса . В частности, как только вы начнете использовать его B
для других различных целей, может возникнуть необходимость изменить или расширить его, чтобы он больше подходил для этих целей. Если есть риск, который B
может подвергнуть методы, которые могут привести к недопустимому состоянию, A
мы должны использовать композицию вместо наследования. Даже если мы являемся автором B
и того A
, и другого, беспокоиться не о чем, поэтому композиция облегчает повторное использование B
.
Мы можем даже утверждать, что если есть функции, в B
которых A
нет необходимости (и мы не знаем, может ли эти функции привести к недопустимому состоянию A
, как в настоящей реализации, так и в будущем), будет хорошей идеей использовать композицию вместо наследства.
Композиция также имеет такие преимущества, как возможность переключения реализаций и упрощение насмешек.
Примечание : есть ситуации, когда мы хотим использовать композицию, несмотря на то, что замена действительна. Мы заархивируем эту заменяемость , используя интерфейсы или абстрактные классы (которые следует использовать, когда это другая тема), а затем используем композицию с внедрением зависимостей реальной реализации.
Наконец, конечно, есть аргумент, что мы должны использовать композицию для защиты родительского класса, потому что наследование нарушает инкапсуляцию родительского класса:
Наследование предоставляет подклассу детали реализации его родителя, часто говорят, что «наследование нарушает инкапсуляцию»
- Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения, Gang of Four
Ну, это плохо разработанный родительский класс. Вот почему вы должны:
Придумай наследство или запрети его.
- Эффективная Java, Джош Блох
Йо-йо проблема
Другой случай, когда помогает композиция - это проблема Йо-йо . Это цитата из Википедии:
В разработке программного обеспечения проблема йо-йо - это анти-паттерн, который возникает, когда программист должен прочитать и понять программу, граф наследования которой настолько длинный и сложный, что программист должен постоянно переключаться между многими различными определениями классов, чтобы следовать управление потоком программы.
Например, вы можете решить: ваш класс C
не будет наследоваться от класса B
. Вместо этого ваш класс C
будет иметь член типа A
, который может быть или не быть (или иметь) объект типа B
. Таким образом, вы будете программировать не против деталей реализации B
, а против контракта, предлагаемого интерфейсом A
.
Счетчик примеров
Многие фреймворки предпочитают наследование по составу (что противоположно тому, что мы обсуждали). Разработчик может сделать это, потому что он вложил много работы в свой базовый класс, чтобы его реализация с использованием композиции увеличила бы размер клиентского кода. Иногда это связано с ограничениями языка.
Например, среда PHP ORM может создать базовый класс, который использует магические методы, чтобы позволить писать код, как если бы объект имел реальные свойства. Вместо этого код, обрабатываемый магическими методами, будет отправляться в базу данных, запрашивать конкретное запрошенное поле (возможно, кешировать его для будущего запроса) и возвращать его. Выполнение этого с композицией потребует от клиента либо создания свойств для каждого поля, либо написания некоторой версии кода магических методов.
Приложение : Есть несколько других способов расширения объектов ORM. Таким образом, я не думаю, что наследование необходимо в этом случае. Это дешевле.
В другом примере движок видеоигры может создать базовый класс, который использует собственный код в зависимости от целевой платформы для выполнения 3D-рендеринга и обработки событий. Этот код сложен и зависит от платформы. В действительности, для пользователя-разработчика движка было бы дорого и подвержено ошибкам иметь дело с этим кодом, что является частью причины использования движка.
Кроме того, без части 3D-рендеринга, это то, сколько работает каркас виджетов. Это избавляет вас от беспокойства по поводу обработки сообщений ОС… на самом деле, во многих языках вы не можете писать такой код без какой-либо формы родного связывания. Более того, если бы вы сделали это, это ограничило бы вашу мобильность. Вместо этого, с наследованием, при условии, что разработчик не нарушает совместимость (слишком много); в будущем вы сможете легко перенести свой код на любые новые платформы, которые они поддерживают.
Кроме того, учтите, что много раз мы хотим переопределить только несколько методов и оставить все остальное с реализациями по умолчанию. Если бы мы использовали композицию, нам пришлось бы создавать все эти методы, даже если бы они были делегированы для обернутого объекта.
Благодаря этому аргументу существует точка, в которой композиция может быть худшей для удобства обслуживания, чем наследования (когда базовый класс слишком сложен). Тем не менее, помните, что поддерживаемость наследования может быть хуже, чем у композиции (когда дерево наследования слишком сложное), что я и упоминаю в проблеме йо-йо.
В представленных примерах разработчики редко намерены повторно использовать код, сгенерированный посредством наследования, в других проектах. Это смягчает снижение возможности повторного использования наследования вместо композиции. Кроме того, используя наследование, разработчики фреймворка могут предоставить множество простых в использовании и легко обнаружить код.
Последние мысли
Как видите, в некоторых ситуациях композиция имеет некоторое преимущество перед наследованием, но не всегда. Для принятия решения важно учитывать контекст и различные вовлеченные факторы (такие как возможность повторного использования, ремонтопригодность, тестируемость и т. Д.). Возвращаясь к первому пункту: «Предпочитаю композицию наследованию» - это просто хорошая эвристика.
Вы также можете заметить, что многие из описанных мною ситуаций могут быть в некоторой степени разрешены с помощью черт или миксинов. К сожалению, это не общие черты в большом списке языков, и они обычно идут с некоторыми затратами на производительность. К счастью, их популярные методы расширения двоюродного брата и реализации по умолчанию смягчают некоторые ситуации.
У меня есть недавняя статья, в которой я рассказываю о некоторых преимуществах интерфейсов, почему нам нужны интерфейсы между пользовательским интерфейсом, бизнесом и доступом к данным в C # . Это может помочь вам развязать и облегчить возможность повторного использования и тестирования.