C#: события или интерфейс наблюдателя? За и против?

У меня есть следующее (упрощенное):

interface IFindFilesObserver
{
    void OnFoundFile(FileInfo fileInfo);
    void OnFoundDirectory(DirectoryInfo directoryInfo);
}

class FindFiles
{
    IFindFilesObserver _observer;

    // ...
}

... и я в противоречии. Это в основном то, что я написал бы на C++, но в C# есть события. Должен ли я изменить код для использования событий или оставить его в покое?

Каковы преимущества или недостатки событий по сравнению с традиционным интерфейсом наблюдателя?


person Roger Lipscombe    schedule 15.02.2009    source источник


Ответы (11)


Рассмотрим событие как интерфейс обратного вызова, где интерфейс имеет только один метод.

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

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

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

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

Синтаксис
Некоторым людям не нравится способ объявления типа делегата для каждого события. Кроме того, стандартные обработчики событий в .NET Framework имеют следующие параметры: (отправитель объекта, аргументы EventArgs). Поскольку отправитель не указывает конкретный тип, вам необходимо выполнить приведение вниз, если вы хотите его использовать. На практике это часто хорошо, но кажется не совсем правильным, потому что вы теряете защиту статической системы типов. Но если вы реализуете свои собственные события и не следуете соглашению платформы .NET, вы можете использовать правильный тип, чтобы не требовалось потенциальное приведение вниз.

person Scott Langham    schedule 15.02.2009
comment
Небольшой плюс интерфейсов заключается в том, что когда у вас много событий, которые обычно собираются вместе, вам не нужно регистрировать их все в огромном блоке событий +=, а нужно регистрировать только один интерфейс. Хотя, конечно, я предпочитаю события :) - person configurator; 15.02.2009
comment
Одним из недостатков событий является то, что они не запускаются в доменах приложений. Если ваше приложение является (или будет) сложным, это станет проблемой. - person TMN; 27.10.2010
comment
События хороши для фреймворков, потому что они обычно не меняются часто, и к тому времени, когда люди начнут их использовать, они должны быть относительно стабильными. Это может быть не так при разработке приложения с быстро меняющимися спецификациями. Предположим, что поведение класса изменилось и требуется новое событие. Вам нужно будет изучить каждое использование класса. Используя интерфейс наблюдателя, компилятор сообщит вам, где требуется внимание, но если вы используете события, вам будет гораздо труднее. - person Christo; 27.04.2011
comment
Малоизвестный факт; на самом деле вам не нужно объявлять тип делегата для событий. Вы можете просто использовать event Action или event Action<string,int> и т. д. - person natli; 18.07.2013
comment
Я не согласен с вашей точкой зрения против интерфейсов, утверждающей, что интерфейсы заставляют вас реализовывать каждый метод/реквизит, определенный в интерфейсе, поскольку это явное нарушение принципа разделения интерфейса. Наличие четко определенных одноцелевых интерфейсов не приведет к именно этой проблеме, о которой вы упомянули. - person Operatorius; 19.09.2018
comment
@Operatorius Вы можете разделить интерфейс в вопросе на интерфейсы IFileFound и IFolderFound, но это похоже на увеличение количества интерфейсов. Было бы лучше использовать события вместо интерфейсов для каждого из них, не так ли? - person Scott Langham; 20.09.2018
comment
А как насчет асинхронности/ожидания? События, похоже, усложняют это - person Joe Phillips; 01.11.2019

Хм, события можно использовать для реализации паттерна Observer. По сути, использование событий можно рассматривать как еще одну реализацию паттерна-обозревателя imho.

person Frederik Gheysels    schedule 15.02.2009
comment
Абсолютно. Это немного похоже на вопрос: должен ли я реализовать шаблон итератора или использовать foreach и IEnumerable? - person Jon Skeet; 15.02.2009
comment
События, запускаемые вашим наблюдателем, — это естественный способ уведомить нескольких потребителей об изменении в приложении. - person JoshBerke; 15.02.2009
comment
Я знаю, что это паттерн наблюдателя; Я спросил, должен ли я реализовать шаблон с помощью интерфейса обратного вызова или с помощью событий. - person Roger Lipscombe; 08.12.2009
comment
Я считаю, что использование событий обычно является лучшим вариантом, потому что по умолчанию оно разделяет оба компонента и косвенно поддерживает более одного источника событий в будущем без изменения какого-либо кода. - person Rahul Garg; 12.05.2017

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

  • Также прочитайте MSDN о новом (в версии 4.0) IObserver<T> интерфейсе.

Рассмотрим этот пример:

using System;

namespace Example
{
    //Observer
    public class SomeFacade
    {
        public void DoSomeWork(IObserver notificationObject)
        {
            Worker worker = new Worker(notificationObject);
            worker.DoWork();
        }
    }
    public class Worker
    {
        private readonly IObserver _notificationObject;
        public Worker(IObserver notificationObject)
        {
            _notificationObject = notificationObject;
        }
        public void DoWork()
        {
            //...
            _notificationObject.Progress(100);
            _notificationObject.Done();
        }
    }
    public interface IObserver
    {
        void Done();
        void Progress(int amount);
    }

    //Events
    public class SomeFacadeWithEvents
    {
        public event Action Done;
        public event Action<int> Progress;

        private void RaiseDone()
        {
            if (Done != null) Done();
        }
        private void RaiseProgress(int amount)
        {
            if (Progress != null) Progress(amount);
        }

        public void DoSomeWork()
        {
            WorkerWithEvents worker = new WorkerWithEvents();
            worker.Done += RaiseDone;
            worker.Progress += RaiseProgress;
            worker.DoWork();
            //Also we neede to unsubscribe...
            worker.Done -= RaiseDone;
            worker.Progress -= RaiseProgress;
        }
    }
    public class WorkerWithEvents
    {
        public event Action Done;
        public event Action<int> Progress;

        public void DoWork()
        {
            //...
            Progress(100);
            Done();
        }
    }
}
person Alex Burtsev    schedule 27.03.2012

Плюсы интерфейсного решения:

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

Минусы:

  • При планировании нужно больше думать, поскольку изменение интерфейса наблюдателя может привести к изменениям во всем вашем решении, что может потребовать другого планирования. Поскольку простое событие является необязательным, другой код практически не должен изменяться, если только этот другой код не должен реагировать на событие.
person Lasse V. Karlsen    schedule 15.02.2009

Некоторые дополнительные преимущества событий.

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

Вы можете достичь всего этого (кроме цепочки инструментов) самостоятельно, но это на удивление сложно. Например: если вы используете переменную-член, например List‹>, для хранения списка наблюдателей. Если вы используете foreach для итерации по нему, то любая попытка добавить или удалить подписчика в одном из обратных вызовов метода OnFoo() вызовет исключение, если вы не напишете дополнительный код для его корректной обработки.

person ShuggyCoUk    schedule 15.02.2009
comment
Какие асинхронные API он предоставляет? - person Joe Phillips; 01.11.2019

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

Минусы в том, что событие означает только одно событие - вам нужно отдельное событие для каждой «вещи», о которой вы хотите уведомить наблюдателя. На самом деле это не имеет большого практического значения, за исключением того, что каждый наблюдаемый объект должен будет содержать ссылку для каждого наблюдателя для каждого события, раздувая память в случае, когда имеется много наблюдаемых объектов (одна из причин, по которой они сделали другой способ хранения). управление отношениями наблюдатель/наблюдаемый в WPF).

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

person U62    schedule 15.02.2009
comment
Ну, ничто не мешает вам иметь одно событие С#, которое различает разные события по дочернему классу EventArgs - хотя я бы этого не сделал. - person Tamas Czinege; 15.02.2009

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

Мы можем подкинуть вам сотню советов. События лучше всего подходят, когда ожидается, что наблюдатель будет прослушивать произвольные события. Интерфейс лучше всего подходит, когда ожидается, что наблюдатель будет перечислять все заданное множество событий. События лучше всего подходят для приложений с графическим интерфейсом. Интерфейсы потребляют меньше памяти (один указатель на несколько событий). Ядда ядда ядда. Маркированный список плюсов и минусов — это повод задуматься, но не окончательный ответ. Что вам действительно нужно сделать, так это попробовать оба из них в реальных приложениях и получить хорошее представление о них. Затем вы можете выбрать тот, который лучше подходит к ситуации. Научитесь делать форму.

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

person snarf    schedule 15.02.2009
comment
Поэтому и спросил плюсы/минусы. Я понимаю, что универсального решения нет. Это было просто то, что я взвешивал и думал, что это будет интересный вопрос. - person Roger Lipscombe; 15.02.2009

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

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

person Daniel Earwicker    schedule 15.02.2009

Преимущество интерфейсов в том, что к ним легче применять декораторы. Стандартный пример:

subject.RegisterObserver(new LoggingObserver(myRealObserver));

по сравнению с:

subject.AnEvent += (sender, args) => { LogTheEvent(); realEventHandler(sender, args); };

(Я большой поклонник шаблона декоратора).

person Andreas    schedule 27.10.2010
comment
Точно так же легко применить шаблон декоратора к интерфейсу. Просто используйте инструмент повышения производительности с вашей IDE, если весь набор текста становится проблемой. - person Christo; 27.04.2011

Я предпочитаю решение на базе событий по следующим причинам.

  • Это снижает стоимость входа. Гораздо проще сказать «+= новый обработчик событий», чем реализовать полноценный интерфейс.
  • Это снижает затраты на техническое обслуживание. Если вы добавите новое событие в свой класс, это все, что нужно сделать. Если вы добавляете новое событие в интерфейс, вы должны обновить каждого потребителя в своей кодовой базе. Или определите совершенно новый интерфейс, который со временем станет раздражать потребителей. «Я должен реализовать IRandomEvent2 или IRandomEvent5?»
  • События позволяют обработчикам быть не основанными на классе (т. е. где-то статическим методом). Нет функциональной причины заставлять все обработчики событий быть членами экземпляра
  • Группировка набора событий в интерфейсе делает предположение о том, как эти события используются (и это просто предположение)
  • Интерфейсы не дают реального преимущества перед необработанным событием.
person JaredPar    schedule 15.02.2009

Если ваши объекты должны быть сериализованы каким-то образом, сохраняющим ссылки, например, с помощью NetDataContractSerializer или, возможно, события protobuf не смогут пересечь границу сериализации. Поскольку шаблон наблюдателя опирается не более чем на ссылки на объекты, он может без проблем работать с этим типом сериализации, если это необходимо.

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

person jpierson    schedule 17.03.2011