Имеет ли смысл определять интерфейс, если у меня уже есть абстрактный класс?

12

У меня есть класс с некоторыми функциями по умолчанию / общий доступ. Я использую abstract classдля этого:

public interface ITypeNameMapper
{
    string Map(TypeDefinition typeDefinition);
}

public abstract class TypeNameMapper : ITypeNameMapper
{
    public virtual string Map(TypeDefinition typeDefinition)
    {
        if (typeDefinition is ClassDefinition classDefinition)
        {
            return Map(classDefinition);
        }
        ...

        throw new ArgumentOutOfRangeException(nameof(typeDefinition));
    }

    protected abstract string Map(ClassDefinition classDefinition);
}

Как видите, у меня тоже есть интерфейс ITypeNameMapper. Имеет ли смысл определять этот интерфейс, если у меня уже есть абстрактный класс TypeNameMapperили abstract classего достаточно?

TypeDefinition в этом минимальном примере это тоже абстракция.

Konrad
источник
4
Просто к сведению - в OOD проверка типов в коде указывает на неправильную иерархию классов - она ​​говорит вам о создании производных классов из проверяемого вами типа. Итак, в этом случае вам нужен интерфейс ITypeMapper, который определяет метод Map (), а затем реализует этот интерфейс в классах TypeDefinition и ClassDefinition. Таким образом, ваш ClassAssembler просто перебирает список ITypeMappers, вызывая Map () для каждого, и получает желаемую строку из обоих типов, поскольку каждый из них имеет свою собственную реализацию.
Родни П. Барбати
1
Использование базового класса вместо интерфейса, если в этом нет необходимости, называется «прожигание базы». Если это можно сделать с помощью интерфейса, сделайте это с помощью интерфейса. Абстрактный класс тогда становится полезным дополнением. artima.com/intv/dotnet.html В частности, удаленное взаимодействие требует от вас наследования от MarshalByRefObject, поэтому, если вы хотите получить удаленный доступ, вы не можете получить что-либо еще.
Бен
@ RodneyP.Barbati не будет работать, если я хочу иметь много картографов для того же типа, который я могу переключать в зависимости от ситуации
Конрад
1
@ Зажигай базу, это интересно. Спасибо за драгоценный камень!
Конрад
@Konrad Это неверно - ничто не мешает вам реализовать интерфейс, вызывая другую реализацию интерфейса, следовательно, каждый тип может иметь подключаемую реализацию, которую вы предоставляете через реализацию в типе.
Родни П. Барбати

Ответы:

31

Да, потому что C # не допускает множественного наследования, за исключением интерфейсов.

Поэтому, если у меня есть класс, который является одновременно TypeNameMapper и SomethingelseMapper, я могу сделать:

class MultiFunctionalClass : ITypeNameMapper, ISomethingelseMapper 
{
    private TypeNameMapper map1
    private SomethingelseMapper map2

    public string Map(TypeDefinition typeDefinition) { return map1.Map(typeDefintion);}

    public string Map(OtherDef otherDef) { return map2.Map(orderDef); }
}
Ewan
источник
4
@Liath: одиночная ответственность на самом деле является фактором, объясняющим, почему наследование необходимо. Рассмотрим три возможных черты: ICanFly, ICanRun, ICanSwim. Вам нужно создать 8 классов существ, по одному на каждую комбинацию черт (YYY, YYN, YNY, ..., NNY, NNN). SRP требует, чтобы вы реализовали каждое поведение (полет, бег, плавание) один раз, а затем повторно применили их на 8 существах. Композиция здесь была бы еще лучше, но, игнорируя композицию на секунду, вы уже должны увидеть необходимость наследования / реализации нескольких отдельных многократно используемых поведений из-за SRP.
Флатер
4
@ Конрад, это то, что я имею в виду. Если вы пишете интерфейс и никогда не нуждаетесь в нем, то стоимость составляет 3 LoC. Если вы не пишете интерфейс и найти вам это нужно, то стоимость может быть рефакторинг всего приложения.
Эван
3
(1) Реализация интерфейсов - это наследование? (2) Вопрос и ответ оба должны быть уточнены. "По-разному." (3) Для множественного наследования требуется предупреждающая наклейка. (4) Я мог бы показать вам K's craptacular формальное interfaceзлоупотребление служебным положением .. избыточные реализации, которые должны быть в основе - которые развиваются независимо! Мой любимый: классы с 1 методом interface, каждый из которых реализует свой собственный 1-метод , каждый с одним экземпляром. ... и т. д., и т. д.
radarbob
3
В коде есть невидимый коэффициент трения. Вы чувствуете это, когда вынуждены читать лишние интерфейсы. Затем он нагревается, как космический челнок, врезавшийся в атмосферу, поскольку реализация интерфейса спутывает абстрактный класс. Это Сумеречная зона, и это челнок Челленджер. Принимая свою судьбу, я смотрю на бушующую плазму с бесстрашным, отрешенным удивлением. Беспощадное трение разрывает фасад совершенства, превращая разбитую тушу в производство с громоподобным грохотом.
радаробоб
4
@radarbob Poetic. ^ _ ^ Я бы согласился; Хотя стоимость интерфейсов низка, она не существует. Не так много в их написании, но в ситуации отладки это еще 1-2 шага, чтобы добраться до реального поведения, которое может быстро сложиться, когда вы получите полдюжины уровней абстракции. В библиотеке это обычно того стоит. Но в приложении рефакторинг лучше, чем преждевременное обобщение.
Errorsatz
1

Интерфейсы и абстрактные классы служат разным целям:

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

В вашем примере interface ITypeNameMapperопределяет потребности клиентов и abstract class TypeNameMapperне добавляет никакой ценности.

Джеффри Хейл
источник
2
Абстрактные классы тоже принадлежат клиентам. Я не знаю, что вы подразумеваете под этим. Все, что publicпринадлежит клиенту. TypeNameMapperдобавляет много значения, потому что это спасает разработчика от написания некоторой общей логики.
Конрад
2
Представление интерфейса как interfaceили не имеет значения только в том, что C # запрещает полное множественное наследование.
Дедупликатор
0

Вся концепция интерфейсов была создана для поддержки семейства классов, имеющих общий API.

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

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

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

Родни П. Барбати
источник
2
«Если у вас есть абстрактный класс, ... использование интерфейса четко обозначено». - Мой вывод противоположный: если у вас есть абстрактный класс, используйте его, как если бы он был интерфейсом (если DI не играет с ним, рассмотрите другую реализацию). Только когда это действительно не работает, создайте интерфейс - вы можете сделать это в любое время позже.
Maaartinus
1
То же самое, @maaartinus. И даже не «... как если бы это был интерфейс». Публичные члены класса - это интерфейс. Также то же самое для "Вы можете сделать это в любое время позже"; это относится к основам дизайна 101.
radarbob
@maaartinus: Если кто-то создает интерфейс с самого начала, но везде использует ссылки типа абстрактного класса, существование интерфейса ничего не поможет. Если, однако, клиентский код использует тип интерфейса вместо типа абстрактного класса, когда это возможно, это значительно облегчит работу, если возникнет необходимость в создании реализации, которая внутренне сильно отличается от абстрактного класса. Изменение клиентского кода в этот момент для использования интерфейса будет гораздо более болезненным, чем разработка клиентского кода для этого с самого начала.
суперкат
1
@supercat Я могу согласиться, если ты пишешь библиотеку. Если это приложение, то я не вижу проблем с заменой всех вхождений одновременно. Мой Eclipse может это сделать (Java, а не C #), но если он не может, это просто как find ... -exec perl -pi -e ...и, возможно, исправление тривиальных ошибок компиляции. Моя точка зрения такова: преимущество отсутствия (в настоящее время) бесполезной вещи намного больше, чем возможная будущая экономия.
Maaartinus
Первое предложение будет правильным, если вы разметили interfaceкак код (вы говорите о C # - interface, а не об абстрактном интерфейсе, как о любом наборе классов / функций / и т. Д.) И завершили его "... но все же запретили полное множественное наследование «. Там нет необходимости, interfaceесли у вас есть ми.
Дедупликатор
0

Если мы хотим явно ответить на вопрос, автор говорит: «Имеет ли смысл определять этот интерфейс, если у меня уже есть абстрактный класс TypeNameMapper или достаточно абстрактного класса?»

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

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

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

Родни П. Барбати
источник
-1

Это довольно сложно ответить, не зная остальной части приложения.

Предполагая, что вы используете какую-то модель DI и разработали свой код так, чтобы он был максимально расширяемым (чтобы вы могли TypeNameMapperлегко добавлять новые ), я бы сказал, да.

Причины для:

  • Вы фактически получаете его бесплатно, так как базовый класс реализует то, что interfaceвам не нужно беспокоиться об этом в любых дочерних реализациях
  • Большинство IoC-фреймворков и библиотек Mocking ожидают interfaces. Хотя многие из них работают с абстрактными классами, это не всегда само собой разумеющееся, и я твердо верю, что, по возможности, буду следовать тем же путем, что и остальные 99% пользователей библиотеки.

Причины против:

  • Строго говоря (как вы указали) вам НЕ ДЕЙСТВИТЕЛЬНО нужно
  • Если class/ interfaceизменяется, есть немного дополнительных накладных расходов

Учитывая все это, я бы сказал, что вы должны создать interface. Причины этого весьма незначительны, но, хотя можно использовать тезисы при моделировании и IoC, часто с интерфейсами гораздо проще. В конечном счете, я бы не стал критиковать кодекс коллег, если бы они пошли другим путем.

Liath
источник