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

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

Ответы [ 33 ]

1091 голосов
/ 10 сентября 2008

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

Мой тест на кислотность для вышеперечисленного:

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

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

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

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

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

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

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

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

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

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

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

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

Примером этого является 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;
   }
}

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

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

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

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

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

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

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

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

73 голосов
/ 09 мая 2009

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

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

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

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

55 голосов
/ 23 января 2010

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

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

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

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

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

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

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

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

31 голосов
/ 31 января 2013

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

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

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

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

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

31 голосов
/ 14 сентября 2015

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

...