Должен ли я использовать наследование?

это скорее субъективный вопрос, поэтому я собираюсь превентивно отметить его как сообщество wiki.

в принципе, я обнаружил, что в большинстве моего кода есть много классов, многие из которых используют друг друга, но немногие из них напрямую связаны друг с другом. Я оглядываюсь на свои студенческие годы и думаю о традиционном class Cat : Animal примеры типа, где у вас есть огромные деревья наследования, но я не вижу ничего из этого в моем коде. Мои диаграммы классов выглядят как гигантская паутина, а не как хорошая красивые деревья.

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

Я могу легко понять подклассы с точки зрения традиционных примеров, таких как class Dog : Animal или class Employee : Person, но у меня просто нет ничего очевидного, с чем я имею дело. И все редко бывает так ясно, как class Label : Control. Но когда дело доходит до моделирования реальных объектов в моем коде как иерархии, я понятия не имею, с чего начать.

Итак, я думаю, мои вопросы сводятся к следующему:

  1. нормально ли просто не подкласс или наследовать? Стоит ли мне вообще беспокоиться?
  2. Каковы некоторые стратегии, которые вы должны определить объекты, которые могут извлечь выгоду из наследования?
  3. приемлемо ли всегда наследовать на основе поведение (интерфейсы), а не фактический тип?

11 ответов


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

например, говоря, что FileOpenDialog "is-a"Window имеет смысл, но говоря, что Engine "is-a"Car - Это бред. В этом случае экземпляр Engine внутри Car экземпляр более уместен (можно сказать, что Car " is-implemented-in-terms-of" Engine).

для наследства, см. Часть 1 и Часть 2 "использования и злоупотребления наследством" на gotw.ca.


  1. до тех пор, пока вы не пропустите четкие отношения "есть", это нормально, и на самом деле лучше не наследовать, а использовать композицию.

  2. is-a-лакмусовая бумажка. если (является X A Y?) тогда class X : Y { } else class X { Y myY; } или class Y { X myX; }

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


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

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

интересным продолжением этого вопроса было бы: какие домены программирования имеют тенденцию делать хорошие использование наследства? (Фреймворки UI и db уже упоминались и являются отличными примерами. Другие?)


Я также ненавижу собаку - > млекопитающее - > примеры животных, именно потому, что они не встречаются в реальной жизни.

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

интерфейсы, однако, велики, и вы должны использовать их.


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

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

посмотреть Эффективная Java (пункты 14 и 15). Это дает отличный аргумент, почему вы должны отдавать предпочтение композиции перед наследованием. Он говорит о наследовании и инкапсуляции в целом (с примерами java). Так что это хороший ресурс, даже если вы не используете Ява.

Итак, чтобы ответить на ваши 3 вопроса:

нормально ли просто не подкласс или наследовать? Стоит ли мне вообще беспокоиться? Ans: задайте себе вопрос: действительно ли это отношения "есть-есть"? Возможно ли украшение? Перейти для украшения

// A collection decorator that is-a collection with 
public class MyCustomCollection implements java.util.Collection {
    private Collection delegate;
    // decorate methods with custom code
}

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

допустимо ли всегда наследовать на основе поведения (интерфейсов), а не фактического типа? Ans: в основном да, но вам будет лучше, если супер класс предназначен для наследования и/или под вашим контролем. Или пойти на композицию.


IMHO, вы никогда не должны делать #3, Если вы не создаете абстрактный базовый класс специально для этой цели, и его название дает понять, какова его цель:

class DataProviderBase {...}
class SqlDataProvider : DataProviderBase {...}
class DB2DataProvider : DataProviderBase {...}
class AccountDataProvider : SqlDataProvider {...}
class OrderDataProvider : SqlDataProvider {...}
class ShippingDataProvider : DB2DataProvider {...}

etc.

также следуя этому типу модели, иногда, если вы предоставляете интерфейс (IDataProvider) хорошо также предоставить базовый класс (DataProviderBase), которые будущие потребители могут использовать для удобного доступа к логике, общей для всех / большинства DataProviders в вашей модели приложения.

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


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

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

большую часть времени нас интересует, что может сделать объект, в отличие от того, что он есть.

class Product
class Article
class NewsItem

это NewsItem и Article и Content предметы? Возможно, и вам может быть полезно иметь список контента, который содержит оба Article предметы и NewsItem предметы.

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

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

наследование - это определение природы объектов Интерфейсы - это определение того, что могут делать объекты.


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


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

например, рассмотрим "трансформатор", используемый для массажа данных в желаемую форму. Если вы получаете 3 источника данных в виде CSV-файлов и хотите поместить их в три разные объектные модели (и, возможно, сохранить их в база данных), вы можете создать базу "CSV transformer", а затем переопределить некоторые методы при наследовании от нее для обработки различных конкретных объектов.

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

кроме того, если вы держите слои отдельно (бизнес, данные, презентации и т. д.), ваша диаграмма классов будет проще, и вы можете "визуализировать" те объекты, которые должны быть унаследованы.


Я бы не слишком беспокоился о том, как выглядит ваша диаграмма классов, вещи редко похожи на класс...

лучше задайте себе два вопроса:

  1. работает ли ваш код?

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

Если ответ на (2) Да, вы можете посмотреть, как вы структурировали свой код для увидеть, если есть более разумный способ, но всегда имея в виду, что в конце дня, вы должны быть в состоянии ответить да на вопрос (1)... Довольно код, который не работает, никому не нужен и трудно объяснить руководству.


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