Должен ли я использовать абстрактные или виртуальные методы?

11

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

  • Преимущество «абстрактной» версии состоит в том, что она, вероятно, выглядит чище и заставляет производный класс давать многообещающую осмысленную реализацию.

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

Абстрактная версия:

public abstract class AbstractVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Виртуальная версия:

public class VirtualVersion
{
    public virtual ReturnType Method1()
    {
        return ReturnType.NotImplemented;
    }

    public virtual ReturnType Method2()
    {
        return ReturnType.NotImplemented;
    }
             .
             .
    public virtual ReturnType MethodN()
    {
        return ReturnType.NotImplemented;
    }

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}
Замочить
источник
Почему мы предполагаем, что интерфейс нежелателен?
Энтони Пеграм
Без частичной проблемы трудно сказать, что одно лучше другого.
Кодизм
@Anthony: интерфейс не желателен, потому что есть полезная функциональность, которая также войдет в этот класс.
Данк
4
return ReturnType.NotImplemented? Шутки в сторону? Если вы не можете отклонить не реализованный тип во время компиляции (вы можете использовать абстрактные методы), по крайней мере, выведите исключение.
Ян Худек
3
@Dunk: Здесь они являются необходимыми. Возвращаемые значения останутся непроверенными.
Ян Худек

Ответы:

15

Мой голос, если бы я потреблял ваши вещи, был бы за абстрактные методы. Это идет вместе с «провалиться рано». Во время объявления может быть трудным добавить все методы (хотя любой приличный инструмент рефакторинга сделает это быстро), но, по крайней мере, я сразу же знаю, в чем проблема, и исправлю ее. Я бы предпочел это, чем отлаживать 6 месяцев и изменения 12 человек позже, чтобы понять, почему мы внезапно получаем не реализованное исключение.

Эрик Дитрих
источник
Хороший вопрос относительно непредвиденного получения ошибки NotImplemented. Что является плюсом на абстрактной стороне, потому что вы получите время компиляции вместо ошибки времени выполнения.
Данк
3
+1 - Помимо ранних сбоев, наследники могут сразу увидеть, какие методы им нужно реализовать через «Реализация абстрактного класса», вместо того, чтобы делать то, что, по их мнению, было достаточно, а затем терпеть неудачу во время выполнения.
Теластин
Я принял этот ответ, потому что сбой во время компиляции был плюсом, который я не смог перечислить, и из-за ссылки на использование IDE для автоматической реализации методов быстро.
Данк
25

Виртуальная версия подвержена ошибкам и семантически неверна.

Аннотация говорит: «Этот метод здесь не реализован. Вы должны реализовать его, чтобы этот класс работал»

Виртуальный говорит: «У меня есть реализация по умолчанию, но вы можете изменить меня, если вам нужно»

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

Том Сквайрс
источник
3

Это зависит от использования вашего класса.

Если методы имеют разумную «пустую» реализацию, у вас есть много методов, и вы часто переопределяете только некоторые из них, тогда использование virtualметодов имеет смысл. Например ExpressionVisitor, реализовано таким образом.

В противном случае, я думаю, вы должны использовать abstractметоды.

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

svick
источник
Я хотел бы отметить, что «NotImplementedException» часто указывает на ошибку пропуска, тогда как «NotSupportedException» указывает на явный выбор. Кроме этого, я согласен.
Энтони Пеграм
Стоит отметить, что многие методы определены в терминах «Делай все, что необходимо для выполнения любых обязательств, связанных с Х». Вызов такого метода для объекта, у которого нет элементов, связанных с X, может быть бессмысленным, но все равно будет четко определен. Кроме того, если у объекта могут быть или не быть какие-либо обязательства, связанные с X, в целом чище и эффективнее безоговорочно сказать «Удовлетворить все ваши обязательства, связанные с X», чем сначала спросить, есть ли у него какие-либо обязательства, и условно попросить его их выполнить. ,
Суперкат
1

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

Код изображения, подобный этому:

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public abstract class AbstractVersion : IVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Это решает эти проблемы:

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

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

например.

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public interface IVersion2
{
    ReturnType Method2_1();
}

public abstract class AbstractVersion : IVersion, IVersion2
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();
    public abstract ReturnType Method2_1();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

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

Майкл Шоу
источник
Я проголосовал за предоставление возможности обрабатывать разные версии. Тем не менее, я попытался и попытался снова эффективно использовать интерфейсные классы, как нормальную часть дизайна, и всегда в конечном итоге осознавал, что интерфейсные классы либо предоставляют значение no / small, но фактически запутывают код, а не облегчают жизнь. Очень редко, чтобы у меня было несколько классов, унаследованных от другого, таких как интерфейсный класс, и не существует достаточного количества общности, которой нельзя поделиться. Абстрактные классы, как правило, работают лучше, так как я получаю встроенный разделяемый аспект, который интерфейсы не предоставляют.
Данк
А использование наследования с хорошими именами классов обеспечивает гораздо более интуитивный способ легко понять классы (и, следовательно, систему), чем набор имен классов функциональных интерфейсов, которые трудно связать друг с другом в целом. Кроме того, использование классов интерфейса имеет тенденцию создавать массу дополнительных классов, что усложняет понимание системы еще одним способом.
Данк
-2

Внедрение зависимостей зависит от интерфейсов. Вот короткий пример. В классе Student есть функция CreateStudent, для которой требуется параметр, реализующий интерфейс «IReporting» (с методом ReportAction). После создания ученика он вызывает ReportAction для конкретного параметра класса. Если система настроена на отправку электронного письма после создания учащегося, мы отправляем его в конкретный класс, который отправляет электронное письмо в своей реализации ReportAction, или мы можем отправить другой конкретный класс, который отправляет выходные данные на принтер в его реализации ReportAction. Отлично подходит для повторного использования кода.

Роджер
источник
1
кажется, это не дает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 5 ответах
комнат