Когда мне следует выбирать наследование по интерфейсу при разработке библиотек классов C#?

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

Мне интересно, с какими соображениями дизайна я должен быть осведомлен (или осведомлен, для вас, пользователей США), когда решаете, все ли они должны наследовать от BaseProcessorили внедрить IProcessor в качестве интерфейса.

7 ответов

Решение

Обычно правило выглядит примерно так:

  • Наследование описывает отношения is-a.
  • Реализация интерфейса описывает взаимосвязь Can -Do.

Чтобы выразить это несколько более конкретно, давайте рассмотрим пример. System.Drawing.Bitmap Класс is- изображение (и, как таковой, он наследует от Image класс), но он также может утилизировать, поэтому он реализует IDisposable интерфейс. Он также может выполнять сериализацию, поэтому он реализуется из ISerializable интерфейс.

Но на практике интерфейсы часто используются для симуляции множественного наследования в C#. Если твой Processor класс должен наследовать от чего-то вроде System.ComponentModel.Component, тогда у вас мало выбора, кроме как реализовать IProcessor интерфейс.

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

Для ответов на подобные вопросы я часто обращаюсь к Руководству по проектированию.NET Framework, в котором говорится о выборе между классами и интерфейсами:

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

Основным недостатком интерфейсов является то, что они гораздо менее гибки, чем классы, когда речь идет об эволюции API. После того, как вы отправите интерфейс, набор его членов будет фиксирован навсегда. Любые дополнения к интерфейсу сломали бы существующие типы, реализующие интерфейс.

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

[. , , ]

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

Их общие рекомендации следующие:

  • Поддерживайте определение классов над интерфейсами.
  • Используйте абстрактные классы вместо интерфейсов, чтобы отделить контракт от реализаций. Абстрактные классы, если они определены правильно, допускают одинаковую степень разделения между контрактом и реализацией.
  • Определите интерфейс, если вам нужно предоставить полиморфную иерархию типов значений.
  • Рассмотрим определение интерфейсов для достижения эффекта, аналогичного эффекту множественного наследования.

Крис Андерсон выражает особое согласие с этим последним принципом, утверждая, что:

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

Ричард,

Почему ВЫБИРАЕТЕ между ними? В качестве опубликованного типа у меня будет интерфейс IProcessor (для использования в других местах системы); и если так получится, что ваши различные CURRENT реализации IProcessor имеют общее поведение, то абстрактный класс BaseProcessor был бы действительно хорошим местом для реализации этого общего поведения.

Таким образом, если вам потребуется IProcessor в будущем, который НЕ был бы для сервисов BaseProcessor, он НЕ ДОЛЖЕН иметь его (и, возможно, скрыть его)... но те, кто этого хочет, могут его иметь... вырубая в дублированном коде / понятиях.

Просто мое скромное мнение.

Приветствия. Кит.

Интерфейсы являются "контрактом", они гарантируют, что некоторые классы реализуют желаемый набор членов - свойств, методов и событий -.

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

Когда использовать интерфейсы?

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

Когда использовать базовые классы (конкретные и / или абстрактные)

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


Примеры:

Давайте поговорим о таблицах.

  • Мы принимаем таблицы, пригодные для повторного использования => Это должно быть определено с помощью интерфейса, такого как "IRecyclableTable", гарантирующего, что все таблицы для повторного использования будут иметь метод "Recycle".

  • Мы хотим таблицы рабочего стола => Это должно быть определено с наследованием. "Настольный стол" - это "стол". Все таблицы имеют общие свойства и поведение, и настольные будут одинаковыми, добавляя такие вещи, которые заставляют настольную таблицу работать иначе, чем другие типы таблиц.

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

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

Помимо чистоты дизайна, вы можете наследовать только один раз, поэтому, если вам нужно унаследовать какой-то каркасный класс, вопрос будет спорным.

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

РЕДАКТИРОВАТЬ:

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

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

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

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

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

Аналогичен ControllerBase и IController в ASP.NET Mvc.

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