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

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

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

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

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

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

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

11 ответов

Решение

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

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

Для хорошего обсуждения наследования см. Часть 1 и Часть 2 "Использование и злоупотребления наследования" на gotw.ca.

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

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

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

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

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

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

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

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

См. Эффективная 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
}

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

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

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

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

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

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

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

и т.п.

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

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

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

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

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

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

class Product
class Article
class NewsItem

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам