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

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

Ответы [ 33 ]

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

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

5 голосов
/ 30 декабря 2013

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

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

4 голосов
/ 07 декабря 2011

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

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

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

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

4 голосов
/ 02 декабря 2011

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

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

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

3 голосов
/ 13 мая 2016

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

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

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

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

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

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

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

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

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

3 голосов
/ 31 августа 2013

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

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

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

Чтобы привести еще один пример, подумайте об абстрактных типах данных, Набор целых чисел и Список целых чисел, значения, которые они могут содержать, ограничены целыми числами. Они оба поддерживают набор методов, таких как add (newValue) и size (). И оба они имеют разные свойства (инвариант класса), Наборы не допускают дублирования, в то время как 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) было явно изменено, без сохранения свойств родительского типа. Подумайте с точки зрения клиента ваших классов. Они могут получить набор целых чисел, где ожидается список целых чисел. Клиент может захотеть добавить значение и добавить это значение в список, даже если это значение уже существует в списке. Но она не получит такого поведения, если ценность существует. Большой сюрприз для нее!

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

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

2 голосов
/ 18 марта 2014

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

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

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

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

1 голос
/ 22 сентября 2016

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

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

  1. Устанавливает логическое отношение " IS A" . Если Автомобиль и Грузовик - это два типа Автомобиль (базовый класс), дочерний класс IS A базовый класс.

    т.е.

    Автомобиль - это транспортное средство

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

  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();
     }
} 
1 голос
/ 01 августа 2012

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

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

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

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

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

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

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

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

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

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

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

ССЫЛКА

  1. Какой класс дизайна лучше?
  2. Наследование против агрегации
...