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

12

Недавно мне пришлось обновить абстрактный базовый класс на некоторых OSS, которые я использовал, чтобы он был более тестируемым, сделав их виртуальными (я не мог использовать интерфейс, так как он объединял два). Это заставило меня задуматься о том, должен ли я отмечать все методы, которые мне нужны, как виртуальные, или я должен отмечать все открытые методы / свойства как виртуальные. Я в целом согласен с Роем Ошеровом, что каждый метод должен быть виртуальным, но я наткнулся на эту статью, которая заставила меня задуматься о том, было ли это необходимо или нет . Однако я собираюсь ограничить это абстрактными классами для простоты (особенно сомнительно, должны ли все конкретные публичные методы быть виртуальными).

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

Тем не менее, я хотел спросить, есть ли что-то, о чем я не думаю. Должны ли все открытые методы в абстрактном классе быть виртуальными?

Джастин Пихони
источник
Это может помочь: stackoverflow.com/questions/391483/…
NoChance
1
Может быть, вы должны посмотреть, почему в C # по умолчанию не виртуальный, но в Java это так. Причина, по которой, вероятно, даст вам некоторое представление об ответе.
Даниэль Литтл

Ответы:

9

Однако, если вы уверены, что принцип замены Лискова будет соблюдаться, то почему бы вам не допустить его отмены?

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

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

Обновить

Некоторые конкретные примеры проектов, над которыми я работал:

  1. общение с устаревшей системой мэйнфреймов через различные «экраны». Каждый экран имеет набор полей с фиксированным именем, положением и длиной, содержащих определенные биты данных. Запрос заполняет определенные поля конкретными данными. Ответ возвращает данные в одном или нескольких других полях. Каждая транзакция следует одной и той же базовой логике, но детали различны на каждом экране. Мы использовали шаблонный метод в нескольких различных проектах для реализации фиксированного каркаса логики обработки экрана, в то же время позволяя подклассам определять детали экрана.
  2. экспорт / импорт данных конфигурации в таблицах БД в / из файлов Excel. Опять же, базовая схема обработки файла Excel и вставки / обновления записей БД или выгрузки записей в Excel одинакова для каждой таблицы, но детали каждой таблицы различны. Поэтому Template Method является очень очевидным выбором для устранения дублирования кода и облегчения понимания и сопровождения кода.
  3. Генерация PDF документов. Каждый документ имеет одинаковую структуру, но его содержание каждый раз отличается в зависимости от множества факторов. Опять же, Template Method позволяет легко отделить фиксированный каркас алгоритма генерации от изменяемых деталей, зависящих от конкретного случая. По факту. здесь он применяется даже на нескольких уровнях, поскольку документ состоит из нескольких разделов , каждый из которых состоит из нуля или более полей . Шаблонный метод применяется на 3 различных уровнях здесь.

В первых двух случаях первоначальная унаследованная реализация использовала стратегию , в результате чего появилось много дублирующегося кода, который, разумеется, с годами увеличивал тонкие различия здесь и там, содержал множество дублированных или слегка отличающихся ошибок, и их было очень трудно поддерживать. Рефакторинг к Template Method (и некоторым другим улучшениям, таким как использование аннотаций Java) уменьшил размер кода примерно на 40-70%.

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

Петер Тёрёк
источник
Простое цитирование GoF не является ответом. Вы должны были бы дать реальную причину, чтобы сделать такую ​​вещь.
DeadMG
@DeadMG, я регулярно использовал шаблонный метод в течение своей карьеры, поэтому я подумал, что было очевидно, что это очень практичный и полезный паттерн (так как большинство паттернов GoF ... это не теоретические академические упражнения, а собранные из опыта реального мира). Но, видимо, не все с этим согласны ... поэтому я добавлю пару конкретных примеров к своему ответу.
Петер Тёрёк
2

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

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

class MyAbstractBaseClass
{
protected:
    virtual void OverrideMe() = 0;
public:
    void CallMeFirst();

    void CallMe()
    {
        CallMeFirst();
        OverrideMe();
    }
};
Бен Коттрелл
источник
Я бы сказал, что до тех пор, пока общее поведение остается неизменным, это можно сделать виртуальным. Вы можете заполнить предварительные / последующие условия, используя другую реализацию (база данных или оперативная память). Иначе, это должна быть частная функция?
Джастин Пихони
2

Достаточно, чтобы класс содержал ОДИН виртуальный метод, чтобы класс стал абстрактным. Возможно, вы захотите обратить внимание на то, какие методы вы хотите использовать виртуальные, а какие нет, в зависимости от типа полиморфизма, который вы планируете использовать.

appoll
источник
1

Спросите себя, какой смысл использовать не виртуальный метод в абстрактном классе. Такой метод должен иметь реализацию, чтобы сделать его полезным. Но если у класса есть реализация, может ли он все еще называться абстрактным классом? Даже если язык / компилятор это позволяет, имеет ли это смысл? Лично я так не думаю. У вас будет нормальный класс с абстрактными методами, которые потомки должны реализовать.

Мой основной язык Delphi, а не C #. В Delphi, если вы помечаете метод как абстрактный, вы также должны пометить его как виртуальный, иначе компилятор пожалуется. Я не следил за последними изменениями языка слишком внимательно, но если абстрактные классы находятся или придут в Delphi, я ожидаю, что компилятор будет жаловаться на любые не виртуальные методы, любые частные методы и на любые реализации методов для класса, помеченного как абстрактный на уровне класса.

Марьян Венема
источник
2
Я думаю, что имеет смысл иметь абстрактный класс, который имеет несколько абстрактных методов, некоторые виртуальные методы и некоторые не виртуальные методы. Я думаю, это всегда зависит от того, что именно вы хотите сделать.
свик
3
Сначала я перейду к вашим C # q. Абстрактный метод в C # неявно виртуален и не может иметь реализацию. Что касается вашего первого замечания, абстрактный класс в C # может и должен иметь какую-то реализацию (в противном случае вы должны просто использовать интерфейс). Суть абстрактного класса в том, что он ДОЛЖЕН быть подклассом, однако он содержит логику, которую (теоретически) будут использовать все подклассы. Это сокращает дублирование кода. Я спрашиваю, следует ли закрывать какую-либо из этих реализаций от переопределения (по сути, базовый путь - единственный путь).
Джастин Пихони
@JustinPihony: спасибо. В Delphi, когда вы помечаете абстрактный метод, компилятор будет жаловаться, если вы предоставите реализацию для него. Интересно, как разные языки реализуют концепции по-разному и таким образом создают разные ожидания у своих пользователей.
Марьян Венема
@svick: да, это имеет смысл, я бы не назвал это абстрактным классом, но классом с абстрактными методами ... Но я думаю, это может быть просто моей интерпретацией.
Марьян Венема
@MarjanVenema, но это не терминология, используемая в C #. Если вы помечаете какие-либо методы в классе abstract, вы также должны пометить весь класс abstract.
svick
0

Однако, если вы уверены, что принцип замены Лискова будет соблюдаться, то> почему бы вам не допустить его отмены?

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

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

Telastyn
источник