Предпочитаете композицию наследству? - PullRequest
1468 голосов
/ 08 сентября 2008

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

Ответы [ 33 ]

20 голосов
/ 21 мая 2012

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

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

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

Общие проблемы использования наследования в моем понимании:

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

...

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

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

...

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

17 голосов
/ 08 сентября 2008

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

16 голосов
/ 04 июня 2016

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

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

Когда следует выбирать наследование над композицией?

Всякий раз, когда предложение "Бар - это Foo, а Бар может делать все, что может делать Foo" имеет смысл.

Традиционная мудрость гласит, что если предложение «Бар - это Foo» имеет смысл, то это хороший намек на то, что выбор наследования уместен. Например, собака - это животное, поэтому хороший класс - наследование класса Dog.

К сожалению, этот простой тест "is-a" не надежен. Проблема Круг-Эллипс является отличным контрпримером: даже если круг является эллипсом, плохая идея иметь класс Circle, наследуемый от Ellipse, потому что есть вещи, которые эллипсы могут делать, но круги не может. Например, эллипсы могут растягиваться, а круги - нет. Таким образом, хотя вы можете иметь ellipse.stretch(), вы не можете иметь circle.stretch().

Вот почему лучшим тестом является «Бар - это Foo, , и Бар может делать все, что может делать Foo ». Это действительно означает, что Foo может использоваться полиморфно. Тест «is-a» является лишь необходимым условием для полиморфного использования и обычно означает, что все получатели Foo имеют смысл в Bar. Дополнительный тест «можно все» означает, что все установщики Foo также имеют смысл в Bar. Этот дополнительный тест обычно завершается неудачно, когда класс Bar «is-a» Foo, но добавляет к нему некоторые ограничения, и в этом случае не следует использовать наследование, поскольку Foo не может использоваться полиморфно. Другими словами, наследование связано не с общими свойствами, а с общими функциями. Производные классы должны расширять функциональность базовых классов, а не ограничивать их.

Это эквивалентно принципу подстановки Лискова :

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

14 голосов
/ 21 мая 2009

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

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

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

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

13 голосов
/ 05 октября 2010

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

12 голосов
/ 29 ноября 2010

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

Class Aircraft extends Engine{
  var wings;
}

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

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

Class Aircraft {
  var wings;
  var engine;
}

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

10 голосов
/ 21 мая 2015

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

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

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

Для дальнейших объяснений см. предпочитать композицию наследованию

7 голосов
/ 07 мая 2009

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

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

6 голосов
/ 17 апреля 2013

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

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

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

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

Почему?

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

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

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

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

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

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

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

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

5 голосов
/ 04 июня 2014

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

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

...