Должен ли я использовать наследование? - PullRequest
8 голосов
/ 22 июля 2010

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

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

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

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

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

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

Ответы [ 11 ]

10 голосов
/ 22 июля 2010

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

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

Хорошее обсуждение наследования см. В Часть 1 и Часть 2 «Использование и злоупотребление наследованием» на gotw.ca.

3 голосов
/ 22 июля 2010

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

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

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

3 голосов
/ 22 июля 2010
  1. Пока вы не пропустите четкие отношения 'is a', это нормально, и на самом деле лучше не наследовать, а использовать композицию.

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

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

2 голосов
/ 22 июля 2010

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

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

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

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

Можно ли просто не наследовать или наследовать? Должен ли я быть обеспокоен вообще? Ответ: Задайте себе вопрос, действительно ли это отношения "is-a"? Возможно ли украшение? Перейти на украшение

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

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

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

2 голосов
/ 22 июля 2010

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

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

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

1 голос
/ 22 июля 2010

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

1 голос
/ 22 июля 2010

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

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

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

class Product
class Article
class NewsItem

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

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

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

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

1 голос
/ 22 июля 2010

ИМХО, вы никогда не должны делать # 3, если только вы не создаете абстрактный базовый класс специально для этой цели, и его имя проясняет, какова его цель:

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

и т.д.

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

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

0 голосов
/ 23 июля 2010

ИМХО, основной причиной использования наследования является то, что код, который был написан для работы с объектом базового класса, вместо этого работает с объектом производного класса.

0 голосов
/ 22 июля 2010

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

Скорее задайте себе два вопроса:

  1. Ваш код работает?

  2. Требуется ли много времени для обслуживания? Требует ли изменение иногда менять «один и тот же» код во многих местах?

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

...