Предпочитают состав над наследованием?

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

Ответ 1

Предпочитайте композицию наследованию, так как она более гибкая/ее легко изменить позже, но не используйте подход, всегда составляемый. С составом, легко изменить поведение на лету с Инъекцией/Установкой Зависимости. Наследование более жесткое, так как большинство языков не позволяют вам наследовать от более чем одного типа. Таким образом, гусь более или менее готовится, как только вы получаете от TypeA.

Мой тест на кислотность для вышеуказанного:

  • Желает ли TypeB предоставить полный интерфейс (не все общедоступные методы) TypeA так, чтобы TypeB можно было использовать там, где ожидается TypeA? Обозначает наследование.

    • например Биплан Cessna покажет полный интерфейс самолета, если не больше. Так что это подходит для получения из самолета.
  • Разве TypeB хочет только часть/часть поведения, раскрываемого TypeA? Указывает на необходимость в композиции.

    • например Птице может понадобиться только поведение мухи самолета. В этом случае имеет смысл извлечь его как интерфейс/класс/оба и сделать его членом обоих классов.

Обновление: Только что вернулся к моему ответу, и теперь кажется, что он неполон без конкретного упоминания Барбары Лисков Принцип подстановки Лискова в качестве теста для "Должен ли я наследовать от этого типа?"

Ответ 2

Думайте о сдерживании как о отношениях. У автомобиля "есть" двигатель, у человека "есть" имя и т.д.

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

Я не принимаю кредит на этот подход. Я взял это прямо из Второго издания Code Complete Стива Макконнелла, раздел 6.3.

Ответ 3

Если вы понимаете разницу, легче объяснить.

Процессуальный кодекс

Примером этого является PHP без использования классов (особенно до PHP5). Вся логика кодируется в виде набора функций. Вы можете включить другие файлы, содержащие вспомогательные функции и т.д., И вести свою бизнес-логику, передавая данные по функциям. Это может быть очень сложно управлять, поскольку приложение растет. PHP5 пытается исправить это, предложив больше объектно-ориентированного дизайна.

наследование

Это поощряет использование классов. Наследование является одним из трех принципов проектирования OO (наследование, полиморфизм, инкапсуляция).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Это наследование на работе. "Сотрудник" является "Лицом или наследуется от Лица". Все отношения наследования - это отношения "есть-а". Сотрудник также затеняет свойство Title от Person, то есть Employee.Title вернет Title для Employee, а не Person.

Состав

Композиция пользуется преимуществом над наследованием. Проще говоря, вы бы:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

Композиция обычно имеет отношение "имеет" или "использует". Здесь у класса Employee есть Person. Он не наследуется от Лица, а вместо этого получает объект Person, переданный ему, поэтому он имеет "Личность".

Композиция над наследованием

Теперь скажите, что вы хотите создать тип менеджера, в результате чего вы получите:

class Manager : Person, Employee {
   ...
}

Однако этот пример будет работать нормально, но если Person and Employee объявили Title? Должен ли Manager.Title вернуть "Менеджер операций" или "Г-н"? По составу эта двусмысленность лучше обрабатывается:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Объект Manager состоит из Employee и Person. Поведение заголовка берется у сотрудника. Эта явная композиция устраняет двусмысленность среди других вещей, и вы столкнетесь с меньшим количеством ошибок.

Ответ 4

Со всеми неоспоримыми преимуществами, предоставляемыми наследством, вот некоторые из его недостатков.

Недостатки наследования:

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

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

Ответ 5

Другая, очень прагматичная причина, чтобы предпочесть состав над наследованием, связана с вашей моделью домена и сопоставлением ее с реляционной базой данных. Очень сложно сопоставить наследование модели SQL (вы получаете всевозможные хакерские обходные пути, например создание столбцов, которые не всегда используются, с использованием представлений и т.д.). Некоторые ORML пытаются справиться с этим, но он всегда быстро усложняется. Композицию можно легко смоделировать с помощью отношения внешнего ключа между двумя таблицами, но наследование намного сложнее.

Ответ 6

Короче говоря, я согласен с "Предпочитаю композицию над наследованием", очень часто для меня это звучит как "предпочитает картофель над кока-колой". Есть места для наследования и места для композиции. Вам нужно понять разницу, тогда этот вопрос исчезнет. Для меня это означает, что "если вы собираетесь использовать наследование - подумайте еще раз, скорее всего, вам нужна композиция".

Вы должны отдать предпочтение картофелю по кока-коле, когда хотите есть, и кока-кола на картофеле, когда вы хотите выпить.

Создание подкласса должно означать не просто удобный способ вызова методов суперкласса. Вы должны использовать наследование, если суперкласс класса "is-a" как структурно, так и функционально, когда его можно использовать в качестве суперкласса, и вы собираетесь его использовать. Если это не так - это не наследование, а что-то другое. Композиция - это когда ваши объекты состоят из другого или имеют какое-то отношение к ним.

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

Ответ 7

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

, вероятно, является наихудшей формой связи, которую вы можете иметь

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

Статья, Наследование зла: эпическая ошибка DataAnnotationsModelBinder, проходит пример этого в С#. Он показывает использование наследования, когда композиция должна была использоваться и как ее можно было реорганизовать.

Ответ 8

В Java или С# объект не может изменить свой тип после его создания.

Итак, если ваш объект должен появляться как другой объект или вести себя по-разному в зависимости от состояния объекта или условий, используйте Состав: см. State и Strategy Шаблоны проектирования.

Если объект должен быть одного типа, используйте Наследование или реализуйте интерфейсы.

Ответ 9

Лично я научился всегда предпочитать композицию над наследованием. Не существует проблем с программной проблемой, которую вы можете решить с наследованием, которую вы не можете решить с помощью композиции; хотя в некоторых случаях вам, возможно, придется использовать интерфейсы (Java) или протоколы (Obj-C). Поскольку С++ не знает ничего подобного, вам придется использовать абстрактные базовые классы, что означает, что вы не можете полностью избавиться от наследования в С++.

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

Однако есть недостатки композиции. Если вы вообще пропустите наследование и сосредоточьтесь только на композиции, вы заметите, что вам часто приходится писать пару лишних строк кода, которые не были необходимы, если вы использовали наследование. Вы также иногда вынуждены повторять себя, и это нарушает принцип СУХОЙ (DRY = Do not Repeat Yourself). Также состав часто требует делегирования, а метод просто вызывает другой метод другого объекта без другого кода, окружающего этот вызов. Такие "вызовы с двойным методом" (которые могут легко распространяться на вызовы с тройным или четверным методом и даже дальше) имеют гораздо худшую производительность, чем наследование, где вы просто наследуете метод вашего родителя. Вызов унаследованного метода может быть столь же быстрым, как вызов ненаследуемого, или он может быть немного медленнее, но обычно все же быстрее, чем два последовательных вызова метода.

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

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

Ответ 10

Не нашел удовлетворительного ответа здесь, поэтому я написал новый.

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

Есть два преимущества наследования: подтипирование и подклассы

  • Подтипирование означает соответствие сигнатуре типа (интерфейса), то есть набор API, и можно переопределить часть подписи для достижения политизма подтипов.

  • Подкласс означает неявное повторное использование реализаций методов.

С двумя преимуществами используются две разные цели для наследования: ориентированный на подтипирование и повторное использование кода.

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

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

Подтип должен соответствовать сигнатуре типа, это означает, что композиция всегда должна отображать не меньшее количество API-интерфейсов типа. Теперь компромиссный удар:

  • Наследование обеспечивает простое повторное использование кода, если оно не переопределено, тогда как состав должен перекодировать каждый API, даже если это просто простая делеция делегирования.

  • Наследование предоставляет прямую открытую рекурсию через внутренний полиморфный сайт this, то есть вызов метода переопределения (или даже type) в другой членской функции, открытой или закрытой (хотя обескуражен). Открытая рекурсия может быть смоделирована с помощью композиции, но она требует дополнительных усилий и может не всегда быть жизнеспособной (?). Этот ответ на дублированный вопрос говорит о чем-то подобном.

  • Наследование предоставляет защищенные члены. Это нарушает инкапсуляцию родительского класса, а если используется подклассом, вводится другая зависимость между дочерним элементом и его родителем.

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

  • Композиция имеет преимущество комбинатор-ориентированное программирование, то есть работает таким образом, как составной шаблон.

  • Состав сразу следует за программированием на интерфейс.

  • Композиция имеет преимущество простого множественного наследования.

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

Ответ 11

Мое общее правило: перед использованием наследования подумайте, имеет ли смысл смысл композиции.

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

Более полный и конкретный ответ от Тима Будро от Sun:

Общие проблемы использования наследования, как я вижу, это:

  • Невинные действия могут иметь неожиданные результаты. Классическим примером этого является призыв к переопределяемым методам из суперкласса конструктор, перед тем, как поля экземпляров подклассов были инициализируется. В идеальном мире никто никогда этого не сделает. Это не идеальный мир.
  • Он предлагает извращенные соблазны для подклассов делать предположения о порядке вызовов методов и т.д. - такие предположения, как правило, не быть стабильным, если суперкласс может развиваться со временем. См. Также мой тостер и аналог кофеварки.
  • Классы становятся тяжелее - вы не всегда знаете, что делает ваш суперкласс в своем конструкторе, или сколько памяти он собирается использовать. Таким образом, построение невинного потенциального легкого объекта может быть намного дороже, чем вы думаете, и это может измениться с течением времени, если суперкласс эволюционирует
  • Он поощряет взрыв подклассов. Classloading затраты времени, больше классов стоит памяти. Это может быть не проблема, пока вы не имея дело с приложением в масштабе NetBeans, но там у нас были реальные проблемы с, например, медленными меню, потому что первый дисплей из меню запускается массовая загрузка классов. Мы исправили это, перейдя к более декларативный синтаксис и другие методы, но это требует времени исправить также.
  • Это затрудняет изменение вещей позже - если вы сделали класс общедоступным, замена суперкласса будет разбивать подклассы - это выбор, который, после того, как вы сделали код общедоступным, вы состоите в браке к. Поэтому, если вы не изменяете реальную функциональность суперкласса, вы получите гораздо больше свободы, чтобы изменить вещи позже, если вы а не расширять то, что вам нужно. Возьмем, к примеру, подклассификация JPanel - это обычно неправильно; и если подкласс где-то публично, вы никогда не получите шанс вернуться к этому решению. Если он доступен как JComponent getThePanel(), вы все равно можете это сделать (подсказка: выставлять модели для компонентов внутри вашего API).
  • Иерархии объектов не масштабируются (или их масштабирование намного сложнее, чем планирование вперед) - это классическое "слишком много слоев", проблема. Я расскажу об этом ниже, и как шаблон AskTheOracle может (хотя это может оскорбить пуристов ООП).

...

Мое занятие, что делать, если вы разрешаете наследование, которое вы можете взять с солью:

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

...

все это меньше относится к небольшим проектам, чем к крупным, и меньше для частных классов, чем для публичных

Ответ 12

Почему композиция предпочтительнее наследования?

Смотрите другие ответы.

Когда вы можете использовать наследование?

Часто говорят, что класс Bar может наследовать класс Foo, когда верно следующее предложение:

  1. бар это дурак

К сожалению, приведенный выше тест не является надежным. Вместо этого используйте следующее:

  1. бар это дурак, И
  2. бары могут делать все, что может делать foos.

Первый тест гарантирует, что все получатели Foo имеют смысл в Bar (= общие свойства), а второй тест проверяет, что все сеттеры Foo имеют смысл в Bar (= совместно используемые функции).

Пример 1: Собака → Животное

Собака - это животное, и собаки могут делать все, что могут делать животные (например, дышать, умирать и т.д.). Поэтому класс Dog может наследовать класс Animal.

Пример 2: Круг - / -> Эллипс

Круг - это эллипс, НО круги не могут делать все, что могут делать эллипсы. Например, круги не могут растягиваться, а эллипсы могут. Поэтому класс Circle не может наследовать класс Ellipse.

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

Когда следует использовать наследование?

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

Как правило, я предпочитаю выбирать наследование, а не композицию, когда ожидается, что полиморфное использование будет очень распространено, и в этом случае мощь динамической диспетчеризации может привести к гораздо более читаемому и элегантному API. Например, наличие полиморфного класса Widget в платформах GUI или полиморфного класса Node в библиотеках XML позволяет иметь API, который гораздо более читабелен и интуитивно понятен, чем тот, который был бы у решения, основанного исключительно на композиции.

Принцип замещения Лискова

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

Функции, которые используют указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом

По сути, это означает, что наследование возможно, если базовый класс можно использовать полиморфно, что, как я считаю, эквивалентно нашему тесту "бар - это foo, а бары могут делать все, что может делать foos".

Ответ 13

Наследование очень мощное, но вы не можете его заставить (см. проблема с круглым эллипсом). Если вы действительно не можете быть полностью уверены в истинном соотношении подтипов "is-a", тогда лучше всего пойти с композицией.

Ответ 14

Наследование создает прочную взаимосвязь между подклассом и суперклассом; подкласс должен быть осведомлен о деталях реализации суперкласса. Создание суперкласса намного сложнее, когда вам нужно подумать о том, как его можно расширить. Вы должны тщательно документировать инварианты классов и указывать, какие другие методы переопределяют методы, используемые внутри.

Наследование иногда полезно, если иерархия действительно представляет собой a-a-relationship. Он относится к принципу открытого закрытия, который гласит, что классы должны быть закрыты для модификации, но открыты для расширения. Таким образом, вы можете иметь полиморфизм; иметь общий метод, который имеет дело с супер-типом и его методами, но с помощью динамической отправки вызывается метод подкласса. Это гибко и помогает создавать косвенные действия, которые необходимы в программном обеспечении (меньше знать о деталях реализации).

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

Я бы предложил использовать компоновку по умолчанию. Он более модульный и дает преимущество позднего связывания (вы можете динамически изменять компонент). Также проще тестировать вещи по отдельности. И если вам нужно использовать метод из класса, вы не должны быть в определенной форме (принцип замены Лискова).

Ответ 16

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

Class Aircraft extends Engine{
  var wings;
}

Теперь ваш самолет может начать с закрепленных крыльев и менять их на поворотные крылья на лету. По существу, двигатель с крыльями. Но что, если я захочу изменить двигатель "на лету"?

Либо базовый класс Engine предоставляет мутатору, чтобы изменить его свойства, или я редизайна Aircraft как:

Class Aircraft {
  var wings;
  var engine;
}

Теперь я могу заменить свой движок на лету.

Ответ 17

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

Если между двумя объектами не существует иерархии реального мира, не используйте наследование, а вместо этого используйте композицию. Композиция представляет отношения "HAS".

например. Автомобиль HAS A Колеса, тело, двигатель и т.д. Но если вы наследуете здесь, он становится CAR IS A Wheel - что неверно.

Дополнительные пояснения см. в предпочитают состав над наследованием

Ответ 18

Если вы хотите "скопировать" /выдать API базового класса, вы используете наследование. Когда вы хотите только "скопировать" функциональность, используйте делегирование.

Один из примеров: вы хотите создать Stack из списка. Стек имеет только pop, push и peek. Вы не должны использовать наследование, учитывая, что вам не нужны функции push_back, push_front, removeAt и др. В стеке.

Ответ 19

Эти два способа могут жить вместе прекрасно и фактически поддерживать друг друга.

Композиция просто играет на модуле: вы создаете интерфейс, похожий на родительский, создаете новый объект и делегируете его. Если эти объекты не должны знать друг друга, это вполне безопасный и простой в использовании состав. Здесь так много возможностей.

Однако, если родительскому классу по какой-то причине требуется доступ к функциям, предоставляемым "дочерним классом" для неопытного программиста, может показаться, что это отличное место для использования наследования. Родительский класс может просто называть его собственным абстрактным "foo()" , который перезаписывается подклассом, а затем он может дать значение абстрактной базе.

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

Почему?

Поскольку наследование - это плохой способ перемещения информации.

Здесь композиция имеет реальное преимущество: отношения могут быть отменены: "родительский класс" или "абстрактный рабочий" могут агрегировать любые конкретные "дочерние" объекты, реализующие определенный интерфейс + , любой ребенок может быть установлен в любом другом тип родителя, который принимает его тип. И может быть любое количество объектов, например MergeSort или QuickSort могут сортировать любой список объектов, реализующих абстрактный Compare -interface. Или иначе: любая группа объектов, которые реализуют "foo()" и другую группу объектов, которые могут использовать объекты, имеющие "foo()" , могут играть вместе.

Я могу думать о трех реальных причинах использования наследования:

  • У вас много классов с одним и тем же интерфейсом, и вы хотите сэкономить время на их запись.
  • Вы должны использовать тот же базовый класс для каждого объекта
  • Вам нужно изменить частные переменные, которые не могут быть общедоступными в любом случае

Если они верны, то, вероятно, необходимо использовать наследование.

Нет ничего плохого в использовании причины 1, очень хорошо иметь сплошной интерфейс на ваших объектах. Это можно сделать с помощью композиции или с наследованием, без проблем - если этот интерфейс прост и не изменяется. Обычно наследование здесь довольно эффективно.

Если причина - номер 2, она становится немного сложной. Вам действительно нужен только тот же базовый класс? В общем, просто использование одного и того же базового класса недостаточно хорошо, но это может быть требование вашей структуры, соображения дизайна, которых нельзя избежать.

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

Ответ 20

Помимо того, что a/имеет соображения, нужно также учитывать "глубину" наследования, которое должен пройти ваш объект. Все, что находится за пределами пяти или шести уровней наследования, может вызвать непредвиденные проблемы с литьем и боксом/распаковкой, и в таких случаях может быть разумным составить свой объект вместо этого.

Ответ 21

Если у вас есть отношение is-a между двумя классами (например, собака - собака), вы идете для наследования.

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

Ответ 22

Простым способом понять это было бы то, что наследование должно использоваться, когда вам нужен объект вашего класса, чтобы иметь тот же интерфейс, что и его родительский класс, так что он может поэтому рассматриваться как объект родительского класса (приведение к базовому типу). Более того, вызовы функций для объекта производного класса будут оставаться одинаковыми везде в коде, но конкретный метод для вызова будет определяться во время выполнения (т.е. Реализация низкого уровня отличается, интерфейс высокого уровня остается тем же).

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

Ответ 23

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

Ответ 24

Я согласен с @Pavel, когда он говорит, есть места для композиции и есть места для наследования.

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

  • Является ли ваша класс частью структуры, которая приносит пользу от полиморфизма? Например, если у вас есть класс Shape, который объявляет метод под названием draw(), то нам явно нужны классы Circle и Square для подклассов Shape, так что их клиентские классы будут зависеть от Shape, а не от определенных подклассов.
  • Требуется ли вашему классу повторное использование любых взаимодействий высокого уровня, определенных в другом классе? шаблонный метод шаблона проектирования невозможно реализовать без наследования. Я считаю, что все расширяемые структуры используют этот шаблон.

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

Ответ 25

Чтобы решить этот вопрос с другой точки зрения для новых программистов:

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

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

Звучит здорово, но на практике это почти никогда не срабатывает по одной из следующих причин:

  • Мы обнаруживаем, что есть некоторые другие функции, которые мы хотим, чтобы наши классы имели. Если способ добавления функциональности к классам осуществляется через наследование, мы должны решить - добавим ли мы его в существующий базовый класс, хотя не каждый класс, который наследует от него, нуждается в этой функции? Создаем ли мы еще один базовый класс? Но как насчет классов, которые уже наследуются от другого базового класса?
  • Мы обнаруживаем, что только для одного из классов, наследуемых от нашего базового класса, мы хотим, чтобы базовый класс вел себя немного по-другому. Итак, теперь мы возвращаемся и возимся с нашим базовым классом, возможно, добавляем некоторые виртуальные методы или, что еще хуже, некоторый код, который говорит: "Если я унаследован тип A, сделайте это, но если я унаследован от типа B, сделайте это". Это плохо по многим причинам. Во-первых, каждый раз, когда мы меняем базовый класс, мы эффективно меняем каждый унаследованный класс. Таким образом, мы действительно меняем класс A, B, C и D, потому что нам нужно немного другое поведение в классе A. Как бы мы ни старались, мы можем сломать один из этих классов по причинам, которые не имеют никакого отношения к тем классы.
  • Мы могли бы знать, почему мы решили заставить все эти классы наследовать друг от друга, но это может не (вероятно, не будет) иметь смысл для кого-то, кто должен поддерживать наш код. Мы могли бы заставить их сделать трудный выбор - я делаю что-то действительно уродливое и беспорядочное, чтобы внести необходимые изменения (см. Предыдущую маркерную точку) или просто переписать кучу этого.

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

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

Антидотом является принцип единой ответственности. Подумайте об этом как об ограничении. Мой класс должен сделать одно. Я должен быть в состоянии дать моему классу имя, которое каким-то образом описывает это одно. (Есть исключения во всем, но абсолютные правила иногда лучше, когда мы учимся.) Из этого следует, что я не могу написать базовый класс под названием ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Независимо от того, какую функциональность мне нужно, должен быть в своем классе, а затем другие классы, которые нуждаются в этой функции, могут зависеть от этого класса, а не наследовать от него.

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

Ответ 26

Правило большого пальца, которое я слышал, является наследованием, которое следует использовать, когда его отношение и состав "есть-a", когда оно имеет "has-a". Даже с этим я чувствую, что вы всегда должны склоняться к композиции, потому что это устраняет большую сложность.

Ответ 27

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

Подкласс не совпадает с подтипом. Вы можете создавать подклассы, которые не являются подтипами (и это когда вы должны использовать композицию). Чтобы понять, что такое подтип, давайте начнем объяснять, что такое тип.

Когда мы говорим, что число 5 имеет тип integer, мы утверждаем, что 5 принадлежит множеству возможных значений (в качестве примера см. возможные значения для примитивных типов Java). Мы также заявляем, что существует допустимый набор методов, которые я могу выполнить для значения, такого как сложение и вычитание. И, наконец, мы заявляем, что существует набор свойств, которые всегда выполняются, например, если я добавлю значения 3 и 5, я получу 8 в результате.

Чтобы привести еще один пример, подумайте об абстрактных типах данных, наборе целых чисел и списке целых чисел, значения, которые они могут удерживать, ограничены целыми числами. Они поддерживают набор методов, таких как add (newValue) и size(). И они оба имеют разные свойства (инвариант класса), Sets не позволяет дублировать, в то время как List разрешает дубликаты (конечно, есть другие свойства, которые оба они удовлетворяют).

Подтип также является типом, который имеет отношение к другому типу, называемому родительским типом (или супертипом). Подтип должен удовлетворять свойствам (значениям, методам и свойствам) родительского типа. Отношение означает, что в любом контексте, где ожидается супертип, его можно подменить подтипом, не влияя на поведение выполнения. Давайте посмотрим на некоторый код, чтобы продемонстрировать, что я говорю. Предположим, что я пишу список целых чисел (в каком-то псевдоязыке):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Затем я пишу Набор целых чисел как подкласс списка целых чисел:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Наш класс целых чисел является подклассом List of Integer, но не является подтипом, поскольку он не удовлетворяет всем функциям класса List. Значения и сигнатура методов выполняются, но свойства не являются. Поведение метода add (Integer) явно изменилось, не сохраняя свойства родительского типа. Подумайте, с точки зрения клиента ваших классов. Они могут получить набор целых чисел, где ожидается список целых чисел. Клиент может захотеть добавить значение и получить добавленное значение в список, даже если это значение уже существует в списке. Но она не получит такое поведение, если значение существует. Большой сюрприз для нее!

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

(фрагмент из: правильно использует наследование).

Ответ 28

Состав v/s Наследование - широкий предмет. Нет никакого реального ответа на то, что лучше, поскольку я думаю, что все зависит от дизайна системы.

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

Если тип отношения - это отношение "IS-A", тогда наследование лучше подходит. иначе тип отношения - отношение "HAS-A", тогда композиция будет лучше приближаться.

Его полностью зависит от отношения сущности.

Ответ 29

Как и многие люди, я сначала начну с проверки - существует ли связь "есть-а". Если он существует, я обычно проверяю следующее:

Можно ли создать базовый класс. То есть базовый класс может быть не абстрактный. Если это может быть не абстрактный, я обычно предпочитаю композицию

Например: 1. Бухгалтер - сотрудник. Но я использую не наследование, потому что объект Employee может быть создан.

Например, 2. Книга - это SellingItem. Нельзя создавать экземпляр SellingItem - это абстрактное понятие. Следовательно, я буду использовать inheritacne. SellingItem является абстрактным базовым классом (или интерфейсом на С#)

Что вы думаете об этом подходе?

Кроме того, я поддерживаю @anon ответ в Зачем использовать наследование вообще?

Основная причина использования наследования - это не форма композиции, поэтому вы можете получить полиморфное поведение. Если вам не нужен полиморфизм, вы, вероятно, не должны использовать наследование.

@MatthieuM. говорится в https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448

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

(для полиморфизма)

(для повторного использования кода)

ССЫЛКА

Ответ 30

Пример реального мира, когда композиция лучше, чем наследование.

Предположим, что у вас есть BaseClass, где T вводится конкретным классом.

public BaseClass<T>{}

На первый взгляд это выглядит хорошо, потому что вы можете вводить любой тип, который хотите, и использовать этот тип для изменения поведения свойств внутри класса; где T ссылается так:

public T SomeProperty {get;set;}

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

public class SubClass : BaseClass<string>    

Но что произойдет, если вам нужно, чтобы это базовое свойство обрабатывало разные типы? Создаете ли вы один класс для каждого типа? Вместо этого просто не используйте сдерживание. Вот так:

public class SubClass{
  private BaseClass<List<string>> BaseForList = new BaseClass<List<string>>();
  private BaseClass<List<ofSomeType>> BaseForListOfSometype = new BaseClass<List<ofSomeType>();

}

В приведенном выше примере вы можете видеть, что SubClass открыт для расширения любым необходимым образом. Это может быть, по сути, более чем одна реализация одного базового слоя. Он легко превращается в этот тип, дающий вам очень специфический доступ к типам собственности. Это лучше, чем наследование и создание нескольких классов, полученных из BaseClass, с одной оговоркой...

Адресация на "Содержимые" вещи принимает другую подпись, которая просто предшествует желаемой вещи (SomeProperty) с экземпляром класса. Вот так:

BaseForList.SomeProperty = something;
BaseForListOfSomeType.SomeProperty = somethingelse;

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

BA.SomeProperty = something;
BL.SomeProperty = somethingelse;