Виртуальные методы или события в C # - PullRequest
9 голосов
/ 16 декабря 2009

В настоящее время я пишу небольшую библиотеку C # для упрощения реализации небольших физических симуляций / экспериментов.

Основным компонентом является SimulationForm, который выполняет внутреннюю петлю таймера и скрывает шаблонный код от пользователя. Сам эксперимент будет определен тремя способами:

  1. Init() (Инициализировать все)
  2. Render(Graphics g) (Визуализация текущего состояния симуляции)
  3. Move(double dt) (перенести эксперимент на dt секунды)

Мне просто интересно, как лучше позволить пользователю реализовать эти функции:

1) Виртуальные методы должны быть переопределены формой наследования

protected virtual void Init() {} 
...

или

2) События

public event EventHandler<MoveSimulationEventArgs> Move = ...
...

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

Самое классное в том, что это «нормальная форма», это то, что вы можете написать

partial class frmMyExperiment : SimulationForm {
}

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

Ответы [ 8 ]

17 голосов
/ 16 декабря 2009

Я предпочитаю использовать виртуальные методы в этом случае.

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

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

15 голосов
/ 16 декабря 2009

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

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

Вот некоторые из моих общих замечаний о выборе между этими образцами:

События замечательные:

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

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

Виртуальные методы работают хорошо:

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

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

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

Существует еще одно решение, которое вы могли бы рассмотреть, используя: шаблон стратегии . Попросите вашего класса поддержать назначение объекта (или делегата), чтобы определить, как Render, Init, Move и т.д. должен вести себя Ваш базовый класс может предоставлять реализации по умолчанию, но позволяет внешним потребителям изменять поведение. Хотя аналогично событию, преимущество заключается в том, что вы можете возвращать значение вызывающему коду и применять только одного подписчика.

5 голосов
/ 16 декабря 2009

Небольшая помощь для принятия решения:

Хотите ли вы уведомить другие (несколько) объектов? Тогда используйте события.

Вы хотите сделать возможными различные реализации / использовать полиморфизм? Затем используйте виртуальные / абстрактные методы.

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

3 голосов
/ 16 декабря 2009

Существует еще одна опция, заимствованная из функционального программирования: представить точки расширения как свойства типа Func или Action, и ваши эксперименты таким образом предоставят делегатов.

Например, в вашем симуляторе вы должны иметь:

public class SimulationForm
{
    public Action Init { get; set; }
    public Action<Graphics> Render { get; set; }
    public Action<double> Move { get; set; }

    //...
}

А в симуляции вы бы получили:

public class Simulation1
{
    private SimulationForm form;

    public void Simulation1()
    {
        form = new SimulationForm();
        form.Init = Init;
        form.Render = Render;
        form.Move = Move;
    }

    private void Init()
    {
        // Do Init code here
    }

    private void Render(Graphics g)
    {
        // Do Rendering code here
    }

    private void Move(double dt)
    {
        // Do Move code here
    }
}

Редактировать : Для очень глупого примера именно этой техники, которую я использовал в каком-то закрытом коде (для частного проекта игры), посмотрите мою делегированную коллекцию с питанием сообщение в блоге .

2 голосов
/ 16 декабря 2009

ни.

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

1 голос
/ 16 декабря 2009

Почему интерфейс не является выбором для этого?

0 голосов
/ 29 марта 2011

Мне нравится резюме Л.Бускина. Что касается хрупкой проблемы базового класса, есть один случай, когда я думаю, что защищенные события могут стать претендентом на лучшую практику:

  1. у вас есть события, действительно предназначенные для внутреннего использования класса (в отличие от стратегий, например, таких как Render OP), таких как Initializing или EnabledChanging,
  2. у вас есть потенциал для большой иерархии наследования (глубокая и / или широкая) с несколькими уровнями, которые должны воздействовать на событие ( несколько непубличных потребителей !)
  3. вы не хотите полагаться на каждый производный класс, не забывая вызывать base.OnSomethingHappened (). (Избегайте проблем с хрупким базовым классом .)
  4. порядок обработки события не имеет значения (в противном случае используйте цепочку base.OnSomethingHappened () для управления порядком.)
  5. событие применяется к некоторому внутреннему изменению состояния и не будет иметь значения для внешнего класса (в противном случае используйте публичное событие ). Например, общедоступное событие инициализации обычно представляет собой отменяемое событие, которое происходит до того, как произойдет какая-либо внутренняя инициализация. Защищенное событие инициализации не должно представляться пользователю класса как отменяемое событие, поскольку класс уже может быть частично инициализирован.

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

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

В случае OP, я думаю, что события Init и, возможно, Move могут соответствовать вышеуказанным критериям в зависимости от обстоятельств. Как правило, маловероятно, что когда-либо будет более одного метода Render, поэтому наиболее вероятным здесь может быть абстрактный метод (или пустой виртуальный метод, если этот класс позволяет запускать симуляции без рисования чего-либо), для которого не нужно вызывать какой-либо базовый метод Render. , Стратегия (открытый делегат) для Render была бы полезна, если бы внешние компоненты могли знать, как нарисовать класс для графической цели, возможно, с альтернативным визуальным стилем (то есть 2D против 3D). Это кажется маловероятным для метода Render, но, возможно, может применяться к другим функциям, не упомянутым, особенно к тем, которые применяют алгоритм (метод усреднения линии тренда и т. Д.).

0 голосов
/ 17 декабря 2009

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

Концептуально вы можете использовать события для представления «вывода» класса, кроме методов возврата.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...