Почему предпочитаешь композицию наследству? Какие компромиссы существуют для каждого подхода? Когда следует выбирать наследование над композицией?
Почему предпочитаешь композицию наследству? Какие компромиссы существуют для каждого подхода? Когда следует выбирать наследование над композицией?
Ответы:
Предпочитайте композицию наследованию, так как она более гибкая / ее легко изменить позже, но не используйте подход, всегда составляемый. С составом, легко изменить поведение на лету с Dependency Injection / Setters. Наследование более жесткое, так как большинство языков не позволяют вам наследовать от более чем одного типа. Таким образом, гусь более или менее готовится, как только вы получаете от TypeA.
Мой кислотный тест для вышеупомянутого:
Желает ли TypeB предоставить полный интерфейс (не все общедоступные методы) TypeA так, чтобы TypeB можно было использовать там, где ожидается TypeA? Указывает на наследование .
Разве TypeB хочет только часть / часть поведения, раскрываемого TypeA? Указывает на необходимость в композиции.
Обновление: только что вернулся к моему ответу, и теперь кажется, что он неполон без конкретного упоминания принципа подстановки Лискова Барбары в качестве теста для «Должен ли я наследовать от этого типа?»
Думайте о сдерживании как об отношениях. У автомобиля "есть" двигатель, у человека "есть" имя и т. Д.
Подумайте о наследовании как это взаимоотношения. Автомобиль »- это« транспортное средство, человек »- это« млекопитающее »и т. Д.
Я не принимаю кредит на этот подход. Я взял это прямо из Второго издания Code Complete от Стива Макконнелла , раздел 6.3 .
Если вы понимаете разницу, это легче объяснить.
Примером этого является 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;
}
}
Объект «Менеджер» состоит из сотрудника и сотрудника. Название поведения взято от сотрудника. Эта явная композиция устраняет неоднозначность среди прочего, и вы столкнетесь с меньшим количеством ошибок.
Со всеми неоспоримыми преимуществами наследования, вот некоторые из его недостатков.
Недостатки наследования:
С другой стороны, композиция объектов определяется во время выполнения посредством объектов, получающих ссылки на другие объекты. В таком случае эти объекты никогда не смогут получить доступ к защищенным данным друг друга (без нарушения инкапсуляции) и будут вынуждены соблюдать интерфейс друг друга. И в этом случае зависимости реализации также будут намного меньше, чем в случае наследования.
Другая, очень прагматичная причина, чтобы предпочесть композицию наследованию, связана с вашей моделью предметной области и отображением ее в реляционную базу данных. Действительно сложно сопоставить наследование с моделью SQL (в итоге вы получаете все виды хакерских обходных путей, таких как создание столбцов, которые не всегда используются, использование представлений и т. Д.). Некоторые ORML пытаются справиться с этим, но это всегда быстро усложняется. Композиция может быть легко смоделирована с помощью отношения внешнего ключа между двумя таблицами, но наследование намного сложнее.
Хотя в двух словах я бы согласился с «Предпочитать композицию перед наследованием», очень часто для меня это звучит как «Предпочитаю картошку перед кока-колой». Есть места для наследства и места для композиции. Вам нужно понять разницу, тогда этот вопрос исчезнет. Для меня это действительно означает «если вы собираетесь использовать наследование - подумайте еще раз, скорее всего, вам нужна композиция».
Вы должны отдавать предпочтение картофелю, а не кока-коле, когда хотите есть, и кока-коле, если хотите пить.
Создание подкласса должно означать больше, чем просто удобный способ вызова методов суперкласса. Вы должны использовать наследование, когда подкласс "is-a" суперкласс структурно и функционально, когда он может использоваться в качестве суперкласса, и вы собираетесь использовать его. Если это не так - это не наследство, а что-то еще. Композиция - это когда ваши объекты состоят из других или имеют какое-то отношение к ним.
Так что для меня это выглядит так: если кто-то не знает, нужно ли ему наследование или состав, настоящая проблема в том, что он не знает, хочет ли он пить или есть. Думайте о своей проблемной области больше, лучше понимайте ее.
InternalCombustionEngine
с производным классом GasolineEngine
. Последний добавляет такие вещи, как свечи зажигания, которых не хватает базовому классу, но использование этой вещи в качестве InternalCombustionEngine
заставит свечи зажигания привыкнуть.
Наследование довольно заманчиво, особенно от процедурной земли, и часто выглядит обманчиво элегантно. Я имею в виду, что все, что мне нужно сделать, это добавить один бит функциональности в какой-то другой класс, верно? Ну, одна из проблем заключается в том, что
Ваш базовый класс нарушает инкапсуляцию, предоставляя детали реализации подклассам в форме защищенных членов. Это делает вашу систему жесткой и хрупкой. Более трагический недостаток, однако, заключается в том, что новый подкласс несет в себе весь багаж и мнение о цепочке наследования.
В статье « Наследование есть зло: эпическая ошибка DataAnnotationsModelBinder» приведен пример этого в C #. Это показывает использование наследования, когда состав должен был использоваться и как это могло быть реорганизовано.
В Java или C # объект не может изменить свой тип после его создания.
Итак, если ваш объект должен выглядеть как другой объект или вести себя по-разному в зависимости от состояния или условий объекта, используйте Композицию : см. Шаблоны проектирования состояний и стратегий .
Если объект должен быть одного типа, используйте наследование или реализуйте интерфейсы.
Client
. Затем, новая концепция PreferredClient
всплывает позже. Должен PreferredClient
наследовать Client
? Предпочитаемый клиент - это, в конце концов, клиент, нет? Ну, не так быстро ... как вы сказали, объекты не могут менять свой класс во время выполнения. Как бы вы смоделировали client.makePreferred()
операцию? Возможно, ответ заключается в использовании композиции с отсутствующей концепцией, а Account
может быть?
Client
классов, возможно, есть только один, который инкапсулирует концепцию, Account
которая может быть StandardAccount
или PreferredAccount
...
Не нашел удовлетворительного ответа здесь, поэтому я написал новый.
Чтобы понять, почему « предпочитать композицию наследованию», нам нужно сначала вернуть предположение, опущенное в этой сокращенной идиоме.
Есть два преимущества наследования: подтип и подклассы
Подтипирование означает соответствие сигнатуре типа (интерфейса), то есть набора API, и можно переопределить часть сигнатуры для достижения полиморфизма подтипа.
Подклассирование означает неявное повторное использование реализаций метода.
С этими двумя преимуществами связаны две разные цели наследования: ориентирование на подтипы и повторное использование кода.
Если повторное использование кода является единственной целью, создание подклассов может дать больше, чем ему нужно, т.е. некоторые открытые методы родительского класса не имеют особого смысла для дочернего класса. В этом случае, вместо того, чтобы отдавать предпочтение композиции над наследованием, требуется композиция . Это также то, откуда приходит понятие «есть» против «имеет».
Таким образом, только когда подтип предназначен, то есть, чтобы использовать новый класс позднее полиморфно, мы сталкиваемся с проблемой выбора наследования или композиции. Это предположение опущено в обсуждаемой сокращенной идиоме.
Подтип означает соответствие сигнатуре типа, это означает, что композиция всегда должна представлять не меньшее количество API-интерфейсов типа. Теперь наступают компромиссы:
Наследование обеспечивает простое повторное использование кода, если не переопределено, в то время как композиция должна перекодировать каждый API, даже если это простая задача делегирования.
Наследование обеспечивает прямую открытую рекурсию через внутренний полиморфный сайт this
, то есть вызывает переопределение метода (или даже типа ) в другой функции-члене, публичной или закрытой (хотя и не рекомендуется ). Открытая рекурсия может быть смоделирована с помощью композиции , но она требует дополнительных усилий и не всегда жизнеспособна (?). Этот ответ на дублированный вопрос говорит о чем-то похожем.
Наследование подвергает защищенных членов. Это нарушает инкапсуляцию родительского класса, и, если используется подклассом, вводится другая зависимость между дочерним и его родительским классом.
Композиция имеет Подходят инверсии управления, и его зависимость может быть введен динамически, как показано на декоратор и прокси - шаблона .
Композиция имеет преимущество комбинаторно-ориентированного программирования, то есть работает как составной шаблон .
Композиция сразу следует за программированием интерфейса .
Преимущество композиции заключается в легком множественном наследовании .
Имея в виду вышеупомянутые компромиссы, мы, следовательно, предпочитаем композицию наследованию. Тем не менее, для тесно связанных классов, т. Е. Когда неявное повторное использование кода действительно приносит пользу, или желательна магическая сила открытой рекурсии, наследование должно быть выбором.
Лично я научился всегда отдавать предпочтение композиции, а не наследованию. Нет программной проблемы, которую вы можете решить с помощью наследования, которую вы не можете решить с помощью композиции; хотя вам, возможно, придется использовать интерфейсы (Java) или протоколы (Obj-C) в некоторых случаях. Поскольку C ++ не знает ничего подобного, вам придется использовать абстрактные базовые классы, что означает, что вы не можете полностью избавиться от наследования в C ++.
Композиция часто более логична, она обеспечивает лучшую абстракцию, лучшую инкапсуляцию, лучшее повторное использование кода (особенно в очень больших проектах) и с меньшей вероятностью что-либо сломает на расстоянии только потому, что вы сделали изолированное изменение в любом месте вашего кода. Это также облегчает соблюдение « Принципа единой ответственности », который часто резюмируется как « У класса не должно быть более одной причины для изменения ». Это означает, что каждый класс существует для определенной цели, и он должен есть только методы, которые напрямую связаны с его целью. Кроме того, наличие очень мелкого дерева наследования значительно упрощает обзор, даже когда ваш проект начинает становиться действительно большим. Многие люди думают, что наследство представляет наш реальный мирдовольно хорошо, но это не правда. Реальный мир использует гораздо больше композиции, чем наследования. Практически каждый объект реального мира, который вы можете держать в руке, был составлен из других, меньших объектов реального мира.
Хотя есть и недостатки в композиции. Если вы вообще пропустите наследование и сконцентрируетесь только на композиции, вы заметите, что вам часто приходится писать пару дополнительных строк кода, в которых не было необходимости, если вы использовали наследование. Вы также иногда вынуждены повторяться, и это нарушает принцип СУХОГО(СУХОЙ = Не повторяйся). Кроме того, композиция часто требует делегирования, и метод просто вызывает другой метод другого объекта без какого-либо другого кода, окружающего этот вызов. Такие «двойные вызовы методов» (которые могут легко распространяться на тройные или четырехкратные вызовы методов и даже дальше) имеют гораздо худшую производительность, чем наследование, когда вы просто наследуете метод своего родителя. Вызов унаследованного метода может быть таким же быстрым, как и вызов не унаследованного, или он может быть немного медленнее, но обычно все же быстрее, чем два последовательных вызова метода.
Возможно, вы заметили, что большинство ОО-языков не допускают множественного наследования. Хотя есть пара случаев, когда множественное наследование действительно может что-то купить, но это скорее исключения, чем правило. Всякий раз, когда вы сталкиваетесь с ситуацией, когда вы думаете, что «множественное наследование было бы действительно полезной функцией для решения этой проблемы», вы обычно находитесь в точке, когда вам следует полностью переосмыслить наследование, поскольку даже для этого может потребоваться пара дополнительных строк кода. решение, основанное на композиции, обычно оказывается гораздо более элегантным, гибким и пригодным для будущего.
Наследование - действительно классная особенность, но я боюсь, что оно использовалось последние пару лет. Люди относились к наследованию как к единому молотку, который может все это прибить, независимо от того, был ли это на самом деле гвоздь, болт или что-то совершенно другое.
TextFile
это File
.
Мое общее правило: перед использованием наследования подумайте, имеет ли смысл смысл композиции.
Причина: подклассы обычно означают большую сложность и связанность, то есть сложнее изменять, поддерживать и масштабировать без ошибок.
Гораздо более полный и конкретный ответ Тима Будро из Sun:
Общие проблемы с использованием наследования, как я вижу это:
- Невинные действия могут привести к неожиданным результатам . Классическим примером этого являются вызовы переопределяемых методов из конструктора суперкласса до инициализации полей экземпляров подклассов. В идеальном мире никто бы этого не сделал. Это не идеальный мир.
- Он предлагает извращенные соблазны для субклассеров делать предположения о порядке вызовов методов и тому подобное - такие предположения имеют тенденцию быть нестабильными, если суперкласс может развиваться со временем. Смотрите также мою аналогию с тостером и кофейником .
- Классы становятся тяжелее - вы не обязательно знаете, что делает ваш суперкласс в своем конструкторе, или сколько памяти он собирается использовать. Поэтому создание какого-то невинного потенциального легковесного объекта может оказаться намного дороже, чем вы думаете, и со временем это может измениться, если суперкласс развивается
- Это поощряет взрыв подклассов . Загрузка классов стоит времени, больше классов - памяти. Это может быть не проблема, пока вы не имеете дело с приложением в масштабе NetBeans, но там у нас были реальные проблемы, например, с медленным меню, потому что первое отображение меню вызвало массовую загрузку классов. Мы исправили это, перейдя к более декларативному синтаксису и другим методам, но это также потребовало времени на исправление.
- Это усложняет изменение вещей позже - если вы сделали класс общедоступным, замена суперкласса приведет к поломке подклассов - это выбор, на котором после того, как вы сделали код общедоступным, вы будете женаты. Поэтому, если вы не изменяете реальную функциональность своего суперкласса, вы получаете гораздо больше свободы, чтобы изменить вещи позже, если вы используете, а не расширять то, что вам нужно. Взять, к примеру, создание подклассов JPanel - это обычно неправильно; и если подкласс где-то публичный, у вас никогда не будет возможности пересмотреть это решение. Если к нему обращаются как JComponent getThePanel (), вы все равно можете это сделать (подсказка: показать модели для компонентов в качестве вашего API).
- Иерархии объектов не масштабируются (или сделать их масштабирование позже гораздо сложнее, чем планировать заранее) - это классическая проблема «слишком много слоев». Я расскажу об этом ниже и о том, как шаблон AskTheOracle может решить эту проблему (хотя он может оскорблять пуристов ООП).
...
Мое мнение о том, что делать, если вы разрешаете наследование, которое вы можете взять с зерном соли:
- Не выставляйте никакие поля, кроме констант
- Методы должны быть абстрактными или окончательными.
- Не вызывать методы из конструктора суперкласса
...
все это относится меньше к маленьким проектам, чем к крупным, и меньше к частным классам, чем к публичным
Смотрите другие ответы.
Часто говорят, что класс Bar
может наследовать класс, Foo
когда верно следующее предложение:
- бар это фу
К сожалению, приведенный выше тест не является надежным. Вместо этого используйте следующее:
- бар это дурак, и
- бары могут делать все, что может делать foos.
Первые испытания гарантируют , что все добытчики из Foo
имеет смысла в Bar
(= общие свойства), в то время как второй тест гарантирует , что все сеттера из Foo
имеет смысла в Bar
(= общей функциональности).
Пример 1: Собака -> Животное
Собака - это животное, и собаки могут делать все, что могут делать животные (например, дышать, умирать и т. Д.). Следовательно, класс Dog
может наследовать класс Animal
.
Пример 2: Круг - / -> Эллипс
Круг - это эллипс, НО круги не могут делать все, что могут делать эллипсы. Например, круги не могут растягиваться, а эллипсы могут. Следовательно, класс Circle
не может наследовать классEllipse
.
Это называется проблемой Circle-Ellipse , которая на самом деле не является проблемой, просто явным доказательством того, что одного первого теста недостаточно, чтобы сделать вывод о возможности наследования. В частности, этот пример подчеркивает, что производные классы должны расширять функциональность базовых классов, а не ограничивать их. В противном случае базовый класс не может быть использован полиморфно.
Даже если вы можете использовать наследование, это не значит, что вы должны : использование композиции всегда возможно. Наследование - это мощный инструмент, позволяющий неявное повторное использование кода и динамическую диспетчеризацию, но у него есть несколько недостатков, поэтому составление часто предпочтительнее. Компромиссы между наследованием и составом не очевидны, и, на мой взгляд, лучше всего объяснить в ответе lcn .
Как правило, я склонен выбирать наследование вместо композиции, когда ожидается, что полиморфное использование будет очень распространенным, и в этом случае мощь динамической диспетчеризации может привести к гораздо более читаемому и элегантному API. Например, наличие полиморфного класса Widget
в платформах GUI или полиморфного класса Node
в библиотеках XML позволяет иметь API, который гораздо более читабелен и интуитивно понятен, чем тот, который вы имели бы с решением, основанным исключительно на композиции.
Как вы знаете, другой метод, используемый для определения возможности наследования, называется принципом подстановки Лискова :
Функции, которые используют указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом
По сути, это означает, что наследование возможно, если базовый класс можно использовать полиморфно, что, как я считаю, эквивалентно нашему тесту «бар - это foo, а бары могут делать все, что может делать foos».
computeArea(Circle* c) { return pi * square(c->radius()); }
. Это очевидно сломано, если передано Эллипс (что даже означает радиус ()?). Эллипс не является Кругом, и как таковой не должен происходить от Круга.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Теперь это общее.
width()
и height()
? Что если теперь пользователь библиотеки решит создать еще один класс с именем «EggShape»? Должно ли оно также происходить от "круга"? Конечно, нет. Форма яйца не является кругом, и эллипс также не является кругом, поэтому никто не должен быть производным от круга, поскольку он нарушает LSP. Методы, выполняющие операции над классом Circle *, делают серьезные предположения о том, что такое круг, и нарушение этих предположений почти наверняка приведет к ошибкам.
Наследование очень мощное, но вы не можете его форсировать (см .: проблема круга-эллипса ). Если вы действительно не можете быть полностью уверены в истинных отношениях типа «есть», то лучше пойти с композицией.
Наследование создает прочные отношения между подклассом и суперклассом; Подкласс должен знать о деталях реализации суперкласса. Создать суперкласс гораздо сложнее, когда нужно подумать, как его можно расширить. Вы должны тщательно документировать инварианты классов и указать, какие другие методы могут быть переопределены внутренне.
Наследование иногда полезно, если иерархия действительно представляет собой отношение «есть отношение». Это относится к принципу Open-Closed, который гласит, что классы должны быть закрыты для модификации, но открыты для расширения. Таким образом, вы можете иметь полиморфизм; иметь универсальный метод, который имеет дело с супертипом и его методами, но через динамическую диспетчеризацию вызывается метод подкласса. Это гибко и помогает создавать косвенные ссылки, которые необходимы в программном обеспечении (чтобы меньше знать о деталях реализации).
Наследование легко злоупотребляется и создает дополнительную сложность с жесткими зависимостями между классами. Также понимание того, что происходит во время выполнения программы, становится довольно трудным из-за слоев и динамического выбора вызовов методов.
Я бы предложил использовать сочинение по умолчанию. Он более модульный и дает преимущество позднего связывания (вы можете динамически изменять компонент). Также проще тестировать вещи по отдельности. И если вам нужно использовать метод из класса, вы не обязаны иметь определенную форму (принцип подстановки Лискова).
Inheritance is sometimes useful... That way you can have polymorphism
как жесткую связь между понятиями наследования и полиморфизма (подтип предполагался с учетом контекста). Мой комментарий был предназначен для того, чтобы указать на то, что вы разъясняете в своем комментарии: что наследование - не единственный способ реализации полиморфизма, и на самом деле это не обязательно определяющий фактор при выборе между композицией и наследованием.
Предположим, что у самолета есть только две части: двигатель и крылья.
Тогда есть два способа конструировать самолет класса.
Class Aircraft extends Engine{
var wings;
}
Теперь ваш самолет может начать с фиксированных крыльев
и заменить их на вращающиеся крылья на лету. По сути
это двигатель с крыльями. Но что, если я хотел изменить
двигатель на лету?
Либо базовый класс Engine
предоставляет мутатор для изменения своих
свойств, либо я изменяю дизайн Aircraft
следующим образом:
Class Aircraft {
var wings;
var engine;
}
Теперь я могу заменить свой двигатель на лету.
Вам нужно взглянуть на принцип подстановки Лискова в SOLID принципах проектирования классов Дядей Бобом . :)
Когда вы хотите «скопировать» / выставить API базового класса, вы используете наследование. Если вы хотите только «скопировать» функциональность, используйте делегирование.
Один из примеров этого: вы хотите создать стек из списка. В стеке есть только pop, push и peek. Вы не должны использовать наследование, если вы не хотите использовать функциональность push_back, push_front, removeAt и др. В стеке.
Эти два способа могут прекрасно жить вместе и фактически поддерживать друг друга.
Композиция просто играет модульно: вы создаете интерфейс, похожий на родительский класс, создаете новый объект и делегируете ему вызовы. Если эти объекты не должны знать друг друга, это довольно безопасно и легко использовать композицию. Здесь так много возможностей.
Однако, если родительский класс по какой-то причине нуждается в доступе к функциям, предоставляемым «дочерним классом» для неопытного программиста, может показаться, что это отличное место для использования наследования. Родительский класс может просто вызвать свой собственный абстрактный «foo ()», который перезаписывается подклассом, и затем он может дать значение абстрактной базе.
Это выглядит как хорошая идея, но во многих случаях лучше просто дать классу объект, который реализует foo () (или даже установить значение, предоставленное foo () вручную), чем наследовать новый класс от некоторого базового класса, который требует функция foo () должна быть указана.
Почему?
Потому что наследование - плохой способ передачи информации .
Здесь у композиции есть реальное преимущество: связь может быть обратной: «родительский класс» или «абстрактный работник» могут агрегировать любые конкретные «дочерние» объекты, реализующие определенный интерфейс + любой дочерний объект может быть установлен внутри любого другого типа родителя, который принимает это тип . И может быть любое количество объектов, например, MergeSort или QuickSort могут отсортировать любой список объектов, реализующих абстрактный интерфейс Compare. Или, говоря иначе: любая группа объектов, которые реализуют "foo ()", и другая группа объектов, которые могут использовать объекты, имеющие "foo ()", могут играть вместе.
Я могу думать о трех реальных причинах использования наследования:
Если это так, то, вероятно, необходимо использовать наследование.
Нет ничего плохого в использовании причины 1, это очень хорошая вещь - иметь надежный интерфейс для ваших объектов. Это может быть сделано с использованием композиции или с наследованием, нет проблем - если этот интерфейс прост и не меняется. Обычно наследование здесь достаточно эффективно.
Если причина номер 2, это немного сложно. Вам действительно нужно использовать только один и тот же базовый класс? В общем, просто использование одного и того же базового класса недостаточно, но это может быть требованием вашей структуры, соображением дизайна, которого нельзя избежать.
Однако, если вы хотите использовать закрытые переменные, случай 3, тогда у вас могут быть проблемы. Если вы считаете глобальные переменные небезопасными, то вам следует рассмотреть возможность использования наследования для получения доступа к закрытым переменным, также небезопасным . Имейте в виду, глобальные переменные - это еще не все, что Плохо - базы данных представляют собой большой набор глобальных переменных. Но если вы можете справиться с этим, то это вполне нормально.
Чтобы ответить на этот вопрос с другой точки зрения для новых программистов:
Наследование часто преподается рано, когда мы изучаем объектно-ориентированное программирование, поэтому оно рассматривается как простое решение распространенной проблемы.
У меня есть три класса, которые все нуждаются в некоторой общей функциональности. Так что, если я напишу базовый класс и сделаю так, чтобы все они унаследовали его, тогда все они будут иметь эту функциональность, и мне нужно будет только поддерживать его в одном месте.
Звучит великолепно, но на практике это почти никогда не работает по одной из нескольких причин:
В конце концов, мы связываем наш код несколькими сложными узлами и не получаем от этого никакой выгоды, за исключением того, что мы говорим: «Круто, я узнал о наследовании и теперь я его использовал». Это не значит быть снисходительным, потому что мы все это сделали. Но мы все сделали это, потому что никто не сказал нам не делать этого.
Как только кто-то объяснил мне «отдавай предпочтение композиции, а не наследованию», я вспоминал каждый раз, когда пытался разделить функциональность между классами, используя наследование, и понимал, что в большинстве случаев он работает не очень хорошо.
Противоядием является принцип единой ответственности . Думайте об этом как об ограничении. Мой класс должен сделать одну вещь. Я должен быть в состоянии дать своему классу имя, которое каким-то образом описывает то, что он делает. (Есть исключения для всего, но абсолютные правила иногда лучше, когда мы учимся.) Из этого следует, что я не могу написать базовый класс с именем ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
. Независимо от того, какая отдельная функциональность мне нужна, она должна принадлежать своему классу, и тогда другие классы, которым эта функциональность нужна, могут зависеть от этого класса, а не наследоваться от него.
С риском упрощения, это композиция - составление нескольких классов для совместной работы. И как только мы сформируем эту привычку, мы обнаружим, что она гораздо более гибкая, поддерживаемая и проверяемая, чем использование наследования.
Помимо соображений, необходимо учитывать «глубину» наследования, через которую должен пройти ваш объект. Все, что находится за пределами пяти или шести уровней глубокого наследования, может вызвать неожиданные проблемы приведения и упаковки / распаковки, и в этих случаях может быть целесообразно вместо этого составить ваш объект.
Когда у вас есть отношение is- между двумя классами (например, собака - это собака), вы переходите на наследство.
С другой стороны, если между двумя классами (или у студентов есть курсы) или (курсами для учителей) имеются какие -либо или некоторые прилагательные отношения, вы выбираете композицию.
Простой способ разобраться в этом состоит в том, что наследование следует использовать, когда вам нужно, чтобы объект вашего класса имел тот же интерфейс, что и его родительский класс, чтобы его можно было таким образом рассматривать как объект родительского класса (апкастинг) , Более того, вызовы функций в объекте производного класса везде будут оставаться одинаковыми в коде, но конкретный метод для вызова будет определен во время выполнения (т. Е. Реализация низкого уровня отличается, интерфейс высокого уровня остается тем же).
Композиция должна использоваться, когда вам не нужно, чтобы новый класс имел такой же интерфейс, т.е. вы хотите скрыть определенные аспекты реализации класса, о которых пользователь этого класса не должен знать. Таким образом, композиция в большей степени поддерживает инкапсуляцию (т. Е. Скрывает реализацию), а наследование предназначено для поддержки абстракции (т. Е. Обеспечивает упрощенное представление чего-либо, в данном случае один и тот же интерфейс для ряда типов с различными внутренними компонентами).
Подтипирование является подходящим и более мощным, когда можно перечислить инварианты , иначе используйте композицию функций для расширяемости.
Я согласен с @Pavel, когда он говорит, что есть места для композиции и есть места для наследования.
Я думаю, что наследство следует использовать, если ваш ответ положительный на любой из этих вопросов.
Однако, если ваше намерение заключается в том, чтобы повторно использовать код, скорее всего, композиция - лучший выбор дизайна.
Наследование - это очень мощный механизм повторного использования кода. Но нужно правильно использовать. Я бы сказал, что наследование используется правильно, если подкласс также является подтипом родительского класса. Как упоминалось выше, ключевой момент здесь - принцип подстановки Лискова.
Подкласс не совпадает с подтипом. Вы можете создать подклассы, которые не являются подтипами (и это когда вы должны использовать композицию). Чтобы понять, что такое подтип, давайте начнем объяснять, что такое тип.
Когда мы говорим, что число 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) было явно изменено, не сохранив свойства родительского типа. Подумайте с точки зрения клиента ваших классов. Они могут получить набор целых чисел, где ожидается список целых чисел. Клиент может захотеть добавить значение и добавить его в список, даже если это значение уже существует в списке. Но она не получит такого поведения, если ценность существует. Большой сюрприз для нее!
Это классический пример неправильного использования наследства. Используйте композицию в этом случае.
(фрагмент из: правильно использовать наследование ).
Эмпирическое правило, которое я слышал: наследование следует использовать, когда это отношение «есть», и композицию, когда это «имеет». Даже при этом я чувствую, что вы всегда должны склоняться к композиции, потому что это устраняет много сложности.
Композиция v / s Наследование - это широкая тема. Нет лучшего ответа на вопрос, что лучше, так как я думаю, что все зависит от конструкции системы.
Обычно тип отношений между объектами предоставляет лучшую информацию для выбора одного из них.
Если тип отношения является отношением «IS-A», тогда лучше использовать Inheritance. в противном случае тип отношения - это отношение "HAS-A", тогда состав лучше подходит.
Это полностью зависит от сущности отношений.
Несмотря на то, что композиция является предпочтительной, я хотел бы выделить плюсы наследования и минусы композиции .
Плюсы наследования:
Он устанавливает логическое отношение «есть » . Если автомобиль и грузовик два типа транспортного средства (базовый класс), дочерний класс ЯВЛЯЕТСЯ базовый класс.
т.е.
Автомобиль - это Автомобиль
Грузовик - это транспортное средство
С наследованием вы можете определить / изменить / расширить возможности
Минусы композиции:
Например, если Автомобиль содержит Автомобиль, и если вам необходимо получить цену на Автомобиль , которая была определена в Транспортном средстве , ваш код будет таким
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
Как говорили многие люди, я сначала начну с проверки - существуют ли отношения «есть». Если он существует, я обычно проверяю следующее:
Может ли базовый класс быть создан. То есть может ли базовый класс быть неабстрактным. Если это может быть не абстрактно, я обычно предпочитаю композицию
Например, 1. Бухгалтер является Сотрудником. Но я не буду использовать наследование, потому что объект Employee может быть создан.
Например, 2. Книга является предметом продажи. SellingItem не может быть создан - это абстрактное понятие. Следовательно, я буду использовать наследство. SellingItem - это абстрактный базовый класс (или интерфейс в C #)
Что вы думаете об этом подходе?
Кроме того, я поддерживаю ответ @anon в разделе Почему вообще используется наследование?
Основная причина использования наследования не в форме композиции, а в том, что вы можете получить полиморфное поведение. Если вам не нужен полиморфизм, вам, вероятно, не следует использовать наследование.
@MatthieuM. говорит в /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
Проблема с наследованием заключается в том, что его можно использовать для двух ортогональных целей:
интерфейс (для полиморфизма)
реализация (для повторного использования кода)
ССЫЛКА
Я вижу, что никто не упомянул проблему алмазов , которая может возникнуть при наследовании.
Вкратце, если классы B и C наследуют A и оба переопределяют метод X, а четвертый класс D наследует от обоих B и C и не переопределяет X, какую реализацию XD предполагается использовать?
Википедия предлагает хороший обзор темы, обсуждаемой в этом вопросе.