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

1604

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

Readonly
источник
3
Смотрите также, какой дизайн класса лучше
Maccullt
40
Существует хорошая статья о том , что вопрос здесь . Мое личное мнение таково, что не существует «лучшего» или «худшего» принципа дизайна. Существует «подходящий» и «неадекватный» дизайн для конкретной задачи. Другими словами - я использую как наследование, так и композицию, в зависимости от ситуации. Цель состоит в том, чтобы создать меньший код, который легче читать, использовать повторно и в дальнейшем расширять.
m_pGladiator
1
в одном предложении наследование является публичным, если у вас есть публичный метод, и вы меняете его, это изменяет опубликованный API. если у вас есть состав, и составленный объект изменился, вам не нужно менять опубликованный API.
Томер Бен Дэвид

Ответы:

1189

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

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

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

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

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

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

Gishu
источник
81
Второй пример прямо из книги Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) :) Я очень рекомендую эту книгу всем, кто ищет этот вопрос.
Иешурун
4
Это очень ясно, но он может что-то упустить: «Хочет ли TypeB предоставить полный интерфейс (не все общедоступные методы) TypeA так, чтобы TypeB можно было использовать там, где ожидается TypeA?» Но что, если это правда, и TypeB также предоставляет полный интерфейс TypeC? А что если TypeC еще не смоделирован?
Тристан
4
Вы ссылаетесь на то, что, по моему мнению, должно быть самым базовым тестом: «Должен ли этот объект быть пригодным для использования кодом, который ожидает объекты (что будет) базового типа». Если ответ да, объект должен наследовать. Если нет, то, вероятно, не следует. Если бы у меня были мои барабанщики, языки предоставили бы ключевое слово для ссылки на «этот класс» и предоставили бы средство определения класса, который должен вести себя так же, как другой класс, но не заменять его (такой класс будет иметь все »это класс "ссылки заменены на себя).
суперкат
22
@ Алексей - суть в том, «Могу ли я передать биплан Cessna всем клиентам, которые ожидают самолет, не удивляя их?». Если да, то скорее всего, вы хотите наследства.
Гишу
9
Я на самом деле изо всех сил пытаюсь придумать какие-либо примеры, когда наследование было бы моим ответом, я часто нахожу, что агрегация, композиция и интерфейсы приводят к более элегантным решениям. Многие из приведенных выше примеров могут быть лучше объяснены с помощью этих подходов ...
Стюарт Уэйкфилд
415

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

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

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

Ник Залуцкий
источник
107
Это не всегда идеальный подход, это просто хорошее руководство. Принцип подстановки Лискова гораздо точнее (меньше проваливается).
Билл К
41
«У моей машины есть машина». Если вы считаете это отдельным предложением, а не в контексте программирования, это абсолютно бессмысленно. И в этом весь смысл этой техники. Если это звучит неловко, это, вероятно, неправильно.
Ник Залуцкий
36
@ Ник Конечно, но «Мой автомобиль имеет VehicleBehavior» имеет больше смысла (я думаю, ваш класс «Vehicle» может быть назван «VehicleBehavior»). Таким образом, вы не можете основывать свое решение на «имеет» сравнение «против» и «сравнение», вы должны использовать LSP, или вы будете делать ошибки
Тристан
35
Вместо "это" думать "ведет себя как". Наследование - это наследование поведения, а не только семантика.
ybakos
4
@ybakos «Ведет себя как» может быть достигнуто через интерфейсы без необходимости наследования. Из Википедии : «Реализация композиции поверх наследования обычно начинается с создания различных интерфейсов, представляющих поведение, которое должна демонстрировать система ... Таким образом, поведение системы реализуется без наследования».
DavidRR
210

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

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

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

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

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

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

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

Это наследство на работе. Сотрудник "является" лицом или наследует от лица. Все наследственные отношения являются отношениями "есть". Employee также скрывает свойство 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, а вместо этого получает объект 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
источник
6
Для наследства: двусмысленности нет. Вы реализуете класс Manager на основе требований. Таким образом, вы бы вернули «Менеджер операций», если это соответствует вашим требованиям, в противном случае вы просто использовали бы реализацию базового класса. Также вы можете сделать Person абстрактным классом и тем самым убедиться, что нижестоящие классы реализуют свойство Title.
Радж Рао
68
Важно помнить, что можно сказать «Композиция над наследованием», но это не означает «Композиция всегда по наследству». «Является» означает наследование и приводит к повторному использованию кода. Сотрудник - это человек (у сотрудника нет человека).
Радж Рао
36
Пример сбивает с толку. Работник - это человек, поэтому он должен использовать наследование. Вы не должны использовать композицию для этого примера, потому что это неправильная связь в модели предметной области, даже если технически вы можете объявить ее в коде.
Майкл Фрейдгейм
15
Я не согласен с этим примером. Сотрудник - это Лицо, которое является учебником случая правильного использования наследства. Я также считаю, что «выпускать» переопределение поля заголовка не имеет смысла. Тот факт, что Employee.Title затеняет Person.Title, является признаком плохого программирования. В конце концов, "мистер" а «менеджер операций» действительно относится к одному и тому же аспекту человека (строчными буквами)? Я бы переименовал Employee.Title и, таким образом, мог бы ссылаться на атрибуты Title и JobTitle Employee, которые имеют смысл в реальной жизни. Кроме того, нет никаких оснований для менеджера (продолжение ...)
Радон Росборо
9
(... продолжение) наследовать от Person и Employee - в конце концов, Employee уже наследует от Person. В более сложных моделях, где человек может быть менеджером и агентом, это правда, что множественное наследование может использоваться (осторожно!), Но во многих средах было бы предпочтительно иметь абстрактный класс Role, из которого Manager (содержит сотрудников) он / она управляет), а агент (содержит контракты и другую информацию) наследуется. Затем Сотрудник - это Человек, имеющий несколько ролей. Таким образом, состав и наследование используются правильно.
Радон Росборо
142

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

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

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

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

Galilyou
источник
5
По моему мнению, это один из лучших ответов - я добавлю к этому, что, по моему опыту, попытка переосмыслить ваши проблемы с точки зрения композиции, как правило, приводит к меньшим, более простым, более автономным, более пригодным для повторного использования классам. с более четким, меньшим, более сфокусированным объемом ответственности. Часто это означает, что меньше требуется таких вещей, как внедрение зависимостей или насмешки (в тестах), так как более мелкие компоненты обычно могут стоять самостоятельно. Просто мой опыт. YMMV :-)
mindplay.dk
3
Последний абзац в этом посте действительно мне понравился. Спасибо.
Salx
87

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

Тим Хоулэнд
источник
81

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

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

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

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

Павел Фельдман
источник
5
Правильный инструмент для правильной работы. Молот может лучше колотить вещи, чем гаечный ключ, но это не значит, что следует рассматривать гаечный ключ как «низший молот». Наследование может быть полезным, когда вещи, добавляемые в подкласс, необходимы для того, чтобы объект вел себя как объект суперкласса. Например, рассмотрим базовый класс InternalCombustionEngineс производным классом GasolineEngine. Последний добавляет такие вещи, как свечи зажигания, которых не хватает базовому классу, но использование этой вещи в качестве InternalCombustionEngineзаставит свечи зажигания привыкнуть.
суперкат
61

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

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

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

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

Майк Валенти
источник
Наследование не является хорошим или плохим, это просто особый случай композиции. Где, действительно, подкласс реализует функциональность, аналогичную суперклассу. Если предложенный вами подкласс не повторно реализует, а просто использует функциональные возможности суперкласса, то вы неправильно использовали Inheritance. Это ошибка программиста, а не размышление о наследовании.
iPherian
42

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

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

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

dance2die
источник
10
+1 Я обнаружил все меньше и меньше, что наследование работает в большинстве ситуаций. Я предпочитаю общие / унаследованные интерфейсы и состав объектов .... или это называется агрегацией? Не спрашивайте меня, у меня есть степень EE!
Кенни
Я полагаю, что это наиболее распространенный сценарий, в котором применяется «композиция вместо наследования», поскольку оба могут быть теоретически уместны. Например, в маркетинговой системе у вас может быть понятие Client. Затем, новая концепция PreferredClientвсплывает позже. Должен PreferredClientнаследовать Client? Предпочитаемый клиент - это, в конце концов, клиент, нет? Ну, не так быстро ... как вы сказали, объекты не могут менять свой класс во время выполнения. Как бы вы смоделировали client.makePreferred()операцию? Возможно, ответ заключается в использовании композиции с отсутствующей концепцией, а Accountможет быть?
plalx
Вместо того, чтобы иметь другой тип Clientклассов, возможно, есть только один, который инкапсулирует концепцию, Accountкоторая может быть StandardAccountили PreferredAccount...
plalx
40

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

LCN
источник
34

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

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

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

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

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

Mecki
источник
«Многие люди думают, что наследство представляет наш реальный мир довольно хорошо, но это не правда». Так много всего этого! Вопреки почти всем учебникам по программированию в мире, моделирование реальных объектов как цепочек наследования, вероятно, плохая идея в долгосрочной перспективе. Вы должны использовать наследование только тогда, когда есть невероятно очевидные, врожденные, простые отношения. Как TextFileэто File.
Неонблицер
25

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

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

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

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

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

...

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

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

...

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

Питер Ценг
источник
25

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

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

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

Часто говорят, что класс 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».

Борис Дальштейн
источник
Сценарии круг-эллипс и квадрат-прямоугольник являются плохими примерами. Подклассы неизменно более сложны, чем их суперкласс, поэтому проблема надумана. Эта проблема решается путем обращения отношений. Эллипс происходит от круга, а прямоугольник - от квадрата. Использовать композицию в этих сценариях крайне глупо.
Нечеткая логика
@FuzzyLogic Согласен, но на самом деле мой пост никогда не поддерживает использование композиции в этом случае. Я только сказал, что проблема круга-эллипса является отличным примером того, почему «есть-а» не является хорошим тестом для того, чтобы сделать вывод, что Круг должен быть получен из эллипса. Как только мы пришли к выводу, что на самом деле Circle не должен наследоваться от Ellipse из-за нарушения LSP, тогда возможными вариантами являются инвертирование отношений, или использование композиции, или использование шаблонных классов, или использование более сложного дизайна, включающего дополнительные классы или вспомогательные функции, и т.д. ... и решение, очевидно, должно приниматься в каждом конкретном случае.
Борис Дальштейн
1
@FuzzyLogic И если вам интересно, что бы я защищал для конкретного случая Circle-Ellipse: я бы высказался за отказ от реализации класса Circle. Проблема с инвертированием отношений заключается в том, что они также нарушают LSP: представьте себе функцию computeArea(Circle* c) { return pi * square(c->radius()); }. Это очевидно сломано, если передано Эллипс (что даже означает радиус ()?). Эллипс не является Кругом, и как таковой не должен происходить от Круга.
Борис Дальштейн
computeArea(Circle *c) { return pi * width * height / 4.0; }Теперь это общее.
Нечеткая логика
2
@FuzzyLogic Я не согласен: вы понимаете, что это означает, что класс Circle предвосхитил существование производного класса Ellipse, и поэтому предоставил width()и height()? Что если теперь пользователь библиотеки решит создать еще один класс с именем «EggShape»? Должно ли оно также происходить от "круга"? Конечно, нет. Форма яйца не является кругом, и эллипс также не является кругом, поэтому никто не должен быть производным от круга, поскольку он нарушает LSP. Методы, выполняющие операции над классом Circle *, делают серьезные предположения о том, что такое круг, и нарушение этих предположений почти наверняка приведет к ошибкам.
Борис Дальштейн
19

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

yukondude
источник
15

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

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

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

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

egaga
источник
3
Стоит отметить, что наследование - не единственный способ достижения полиморфизма. Pattern Decorator обеспечивает появление полиморфизма через композицию.
BitMask777
1
@ BitMask777: полиморфизм подтипа - это только один вид полиморфизма, другой - параметрический полиморфизм, для этого вам не нужно наследование. Также более важно: когда речь идет о наследовании, один означает наследование класса; .ie вы можете иметь полиморфизм подтипа, имея общий интерфейс для нескольких классов, и у вас не возникает проблем наследования.
egaga
2
@engaga: я интерпретировал ваш комментарий Inheritance is sometimes useful... That way you can have polymorphismкак жесткую связь между понятиями наследования и полиморфизма (подтип предполагался с учетом контекста). Мой комментарий был предназначен для того, чтобы указать на то, что вы разъясняете в своем комментарии: что наследование - не единственный способ реализации полиморфизма, и на самом деле это не обязательно определяющий фактор при выборе между композицией и наследованием.
BitMask777
15

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

Class Aircraft extends Engine{
  var wings;
}

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

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

Class Aircraft {
  var wings;
  var engine;
}

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

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

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

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

Anzurio
источник
7

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

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

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

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

Почему?

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

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

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

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

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

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

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

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

Теро Толонен
источник
7

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

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

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

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

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

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

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

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

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

Скотт Ханнен
источник
То, что классы не могут использовать несколько базовых классов, является не плохим отражением наследования, а скорее плохим отражением недостаточной способности конкретного языка.
iPherian
За время, прошедшее с момента написания этого ответа, я прочитал этот пост от «Дяди Боба», в котором говорится об отсутствии возможностей. Я никогда не использовал язык, который допускает множественное наследование. Но, оглядываясь назад, вопрос помечается как «независимый от языка», и мой ответ предполагает C #. Мне нужно расширить свой кругозор.
Скотт Ханнен
6

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

Джон Лимджап
источник
6

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

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

Амир Аслам
источник
Вы сказали, что наследство и наследство. Вы не имеете в виду наследование и состав?
trevorKirkby
Нет, ты не Вы также можете определить собачий интерфейс и позволить каждой собаке реализовать его, и в итоге вы получите больше кода SOLID.
Маркус
5

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

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

YS
источник
+1 за упоминание интерфейса. Я часто использую этот подход, чтобы скрыть существующие классы и сделать мой новый класс соответствующим образом тестируемым модулем путем макетирования объекта, используемого для композиции. Это требует от владельца нового объекта передать ему родительский класс-кандидат.
Келл
4

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

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

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

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

Параг
источник
4

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

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

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

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

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

Энрике Молинари
источник
3

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


источник
2

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

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

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

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

Шами Куреши
источник
2

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

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

  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();
     }
} 
Равиндра Бабу
источник
Я думаю, что это не отвечает на вопрос
альманегра
Вы можете пересмотреть вопрос ОП. Я обратился: какие компромиссы существуют для каждого подхода?
Равиндра Бабу
Как вы упомянули, вы говорите только о «плюсах наследования и минусах композиции», а не о компромиссах для КАЖДОГО подхода или случаев, когда вы должны использовать один над другим
almanegra
плюсы и минусы обеспечивают компромисс, поскольку плюсы наследования - это минусы композиции, а минусы композиции - плюсы наследования.
Равиндра Бабу
1

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

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

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

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

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

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

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

@MatthieuM. говорит в /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

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

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

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

ССЫЛКА

  1. Какой класс дизайна лучше?
  2. Наследование против агрегации
LCJ
источник
1
Я не уверен, почему «базовый класс является абстрактным?» фигурирует в обсуждении ... LSP: будут ли работать все функции, которые работают с Dogs, если передаются объекты Poodle? Если да, то Пудель может быть заменен Собакой и, следовательно, может наследоваться от Собаки.
Гишу
@Gishu Спасибо. Я обязательно посмотрю в LSP. Но перед этим, пожалуйста, предоставьте «пример правильного наследования, в котором базовый класс не может быть абстрактным». Я думаю, что наследование применимо, только если базовый класс является абстрактным. Если базовый класс необходимо создать отдельно, не переходите к наследованию. То есть, хотя Бухгалтер является Сотрудником, не используйте наследование.
LCJ
1
в последнее время читал WCF. Примером .NET Framework является SynchronizationContext (можно создать экземпляр base +), который работает в потоке ThreadPool. Производные включают WinFormsSyncContext (очередь в потоке пользовательского интерфейса) и DispatcherSyncContext (очередь в диспетчере WPF)
Gishu
@Gishu Спасибо. Однако было бы более полезно, если бы вы могли предоставить сценарий, основанный на банковском домене, HR-домене, розничном домене или любом другом популярном домене.
LCJ
1
Сожалею. Я не знаком с этими доменами. Другой пример, если предыдущий был слишком тупым, это класс Control в Winforms / WPF. Базовый / общий контроль может быть создан. Производные включают списки, текстовые поля и т. Д. Теперь, когда я об этом думаю, шаблон Designrator Design является хорошим примером ИМХО и полезным. Декоратор происходит от неабстрактного объекта, который он хочет обернуть / украсить.
Гишу
1

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

Вкратце, если классы B и C наследуют A и оба переопределяют метод X, а четвертый класс D наследует от обоих B и C и не переопределяет X, какую реализацию XD предполагается использовать?

Википедия предлагает хороший обзор темы, обсуждаемой в этом вопросе.

Veverke
источник
1
D наследует B и C, а не A. Если это так, то он будет использовать реализацию X, которая находится в классе A.
fabricio
1
@fabricio: спасибо, я редактировал текст. Кстати, такой сценарий не может возникнуть в языках, которые не допускают наследование нескольких классов, я прав?
Веверке
да, вы правы .. и я никогда не работал с тем, который допускает множественное наследование (как в примере с проблемой алмазов) ..
fabricio