Вопрос:

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

language-agnostic oop inheritance composition aggregation

277543 просмотра

30 ответа

137703 Репутация автора

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

Автор: Readonly Источник Размещён: 08.09.2008 01:58

Ответы (30)


381 плюса

10026 Репутация автора

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

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

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

Автор: Nick Zalutskiy Размещён: 08.09.2008 02:09

3 плюса

0 Репутация автора

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

Автор: Evrhet Milam Размещён: 08.09.2008 02:14

40 плюса

19721 Репутация автора

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

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

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

Автор: Sung M. Kim Размещён: 08.09.2008 02:25

18 плюса

15593 Репутация автора

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

Автор: yukondude Размещён: 08.09.2008 02:29

81 плюса

6803 Репутация автора

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

Автор: Tim Howland Размещён: 08.09.2008 02:48

6 плюса

82005 Репутация автора

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

Автор: Jon Limjap Размещён: 08.09.2008 03:00

1115 плюса

105620 Репутация автора

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

Мой кислотный тест для вышеупомянутого:

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

Например, биплан Cessna покажет полный интерфейс самолета, если не больше. Так что это подходит для получения из самолета.

  • Разве TypeB хочет только часть / часть поведения, раскрываемого TypeA? Указывает на необходимость в композиции.

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

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

Автор: Gishu Размещён: 10.09.2008 03:04

7 плюса

13662 Репутация автора

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

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

Автор: Anzurio Размещён: 07.05.2009 01:43

74 плюса

3321 Репутация автора

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

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

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

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

Автор: Pavel Feldman Размещён: 08.05.2009 10:29

202 плюса

23284 Репутация автора

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

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

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

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

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

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

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

Это наследство на работе. Сотрудник "является" лицом или наследует от лица. Все наследственные отношения являются отношениями "есть". Employee также скрывает свойство Title от Person, то есть Employee.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, а вместо этого получает объект Person, передаваемый ему, поэтому у него «есть» Person.

Состав по наследству

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

class Manager : Person, Employee {
   ...
}

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

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

Объект «Менеджер» состоит из сотрудника и сотрудника. Название поведения взято от сотрудника. Эта явная композиция устраняет неоднозначность среди прочего, и вы столкнетесь с меньшим количеством ошибок.

Автор: aleemb Размещён: 21.05.2009 07:54

14 плюса

12447 Репутация автора

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

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

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

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

Автор: egaga Размещён: 21.05.2009 08:11

122 плюса

5670 Репутация автора

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

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

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

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

Автор: Galilyou Размещён: 21.05.2009 08:34

0 плюса

0 Репутация автора

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

Is-aи Has-aэто полезное правило.

Автор: user125959 Размещён: 20.06.2009 04:41

56 плюса

7967 Репутация автора

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

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

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

В статье « Наследование есть зло: эпическая ошибка DataAnnotationsModelBinder» приведен пример этого в C #. Это показывает использование наследования, когда состав должен был использоваться и как это могло быть реорганизовано.

Автор: Mike Valenty Размещён: 23.01.2010 07:55

13 плюса

2656 Репутация автора

Вам нужно взглянуть на принцип подстановки Лискова в SOLID принципах проектирования классов Дядей Бобом . :)

Автор: nabeelfarid Размещён: 05.10.2010 11:57

13 плюса

5475 Репутация автора

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

Class Aircraft extends Engine{
  var wings;
}

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

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

Class Aircraft {
  var wings;
  var engine;
}

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

Автор: dharm0us Размещён: 29.11.2010 10:54

4 плюса

5255 Репутация автора

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

Автор: Shelby Moore III Размещён: 02.12.2011 07:40

4 плюса

5505 Репутация автора

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

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

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

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

Автор: Parag Размещён: 07.12.2011 10:40

21 плюса

9691 Репутация автора

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

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

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

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

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

...

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

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

...

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

Автор: Peter Tseng Размещён: 21.05.2012 01:59

1 плюс

11366 Репутация автора

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

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

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

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

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

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

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

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

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

интерфейс (для полиморфизма)

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

ССЫЛКА

  1. Какой класс дизайна лучше?
  2. Наследование против агрегации
Автор: Lijo Размещён: 01.08.2012 11:15

32 плюса

83246 Репутация автора

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

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

Хотя есть и недостатки в композиции. Если вы вообще пропустите наследование и сконцентрируетесь только на композиции, вы заметите, что вам часто приходится писать пару дополнительных строк кода, в которых не было необходимости, если вы использовали наследование. Вы также иногда вынуждены повторяться, и это нарушает принцип СУХОГО(СУХОЙ = Не повторяйся). Кроме того, для композиции часто требуется делегирование, а метод просто вызывает другой метод другого объекта без какого-либо другого кода, окружающего этот вызов. Такие «двойные вызовы методов» (которые могут легко распространяться на тройные или четырехкратные вызовы методов и даже дальше) имеют гораздо худшую производительность, чем наследование, когда вы просто наследуете метод своего родителя. Вызов унаследованного метода может быть таким же быстрым, как и вызов не унаследованного, или он может быть немного медленнее, но обычно все же быстрее, чем два последовательных вызова метода.

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

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

Автор: Mecki Размещён: 31.01.2013 07:34

7 плюса

3124 Репутация автора

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

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

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

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

Зачем?

Потому что наследование - плохой способ передачи информации .

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

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

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

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

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

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

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

Автор: Tero Tolonen Размещён: 16.04.2013 08:15

3 плюса

229 Репутация автора

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

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

Когда мы говорим, что число 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);
     }
  }
}

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

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

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

Автор: Enrique Molinari Размещён: 31.08.2013 01:41

5 плюса

190 Репутация автора

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

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

Автор: Amir Aslam Размещён: 30.12.2013 01:57

2 плюса

64 Репутация автора

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

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

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

Это полностью зависит от сущности отношений.

Автор: Shami Qureshi Размещён: 18.03.2014 09:39

5 плюса

22838 Репутация автора

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

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

Автор: Y.S Размещён: 04.06.2014 11:36

31 плюса

1406 Репутация автора

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  7. Преимущество композиции заключается в легком множественном наследовании .

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

Автор: lcn Размещён: 14.09.2015 05:22

4 плюса

17345 Репутация автора

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

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

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

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

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

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

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

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

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

Автор: Scott Hannen Размещён: 13.05.2016 08:24

20 плюса

3689 Репутация автора

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

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

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

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

  1. бар это фу

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Автор: Boris Dalstein Размещён: 03.06.2016 10:23

1 плюс

32875 Репутация автора

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

Плюсы наследования:

  1. Он устанавливает логическое отношение «есть » . Если автомобиль и грузовик два типа транспортного средства (базовый класс), дочерний класс ЯВЛЯЕТСЯ базовый класс.

    т.е.

    Автомобиль - это Автомобиль

    Грузовик - это транспортное средство

  2. С наследованием вы можете определить / изменить / расширить возможности

    1. Базовый класс не обеспечивает реализацию, а подкласс должен переопределять завершенный метод (аннотация) => Вы можете реализовать контракт
    2. Базовый класс обеспечивает реализацию по умолчанию, а подкласс может изменить поведение => Вы можете переопределить контракт
    3. Подкласс добавляет расширение к реализации базового класса, вызывая super.methodName () в качестве первого утверждения => Вы можете продлить контракт
    4. Базовый класс определяет структуру алгоритма, а подкласс переопределит часть алгоритма => Вы можете реализовать Template_method без изменений в скелете базового класса

Минусы композиции:

  1. В наследовании подкласс может напрямую вызывать метод базового класса, даже если он не реализует метод базового класса из-за отношения IS A. Если вы используете композицию, вы должны добавить методы в контейнерный класс, чтобы предоставить доступ к API класса.

Например, если Автомобиль содержит Автомобиль, и если вам нужно получить цену на Автомобиль , которая была определена в Транспортном средстве , ваш код будет таким

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 
Автор: Ravindra babu Размещён: 22.09.2016 09:31
32x32