C#:事件或观察者界面?优点缺点?

Rog*_*mbe 58 c# events observer-pattern

我有以下(简化):

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

class FindFiles
{
    IFindFilesObserver _observer;

    // ...
}
Run Code Online (Sandbox Code Playgroud)

......而且我很矛盾.这基本上就是我用C++编写的,但C#有事件.我应该更改代码以使用事件,还是应该不管它?

与传统观察者界面相比,事件的优缺点是什么?

Sco*_*ham 67

将事件视为回调接口,其中接口只有一个方法.

只有您需要的钩子事件
使用事件,您只需要为您感兴趣的事件实现处理程序.在观察者界面模式中,您必须在整个界面中实现所有方法,包括实现您实际上并不关心处理的通知类型的方法体.在您的示例中,您始终必须实现OnFoundDirectory和OnFoundFile,即使您只关心其中一个事件.

减少维护
事件的另一个好处是,您可以向特定类添加一个新的,以便它可以提升它,并且您不必更改每个现有的观察者.而如果要向接口添加新方法,则必须绕过已实现该接口的每个类并在所有接口中实现新方法.但是,对于事件,您只需要更改实际想要执行某些操作的现有类以响应您要添加的新事件.

模式内置于语言中,因此每个人都知道如何使用它
事件是惯用的,因为当您看到事件时,您就知道如何使用它.使用观察者界面,人们经常实现不同的注册方式来接收通知并连接观察者......但是,一旦你学会了如何注册和使用一个(使用+ =运算符),其余的都是相同.

接口的优点
我没有很多接口的专业人士.我猜他们强迫某人在界面中实现所有方法.但是,你不能真正强迫某人正确地实施所有这些方法,所以我认为这没有很多价值.

语法
有些人不喜欢为每个事件声明委托类型的方式.此外,.Net框架中的标准事件处理程序遵循以下参数:( object sender,EventArgs args).由于发件人未指定特定类型,因此如果要使用它,则必须进行向下转换.这在实践中通常很好,如果感觉不太正确,因为你正在失去对静态类型系统的保护.但是,如果您实现自己的事件并且不遵循.Net框架约定,则可以使用正确的类型,因此不需要潜在的向下转换.

  • 事件对于框架是有益的,因为它们通常不经常改变,并且当人们开始使用它们时它们应该相对稳定.在开发具有快速变化的规范的应用程序时可能不是这种情况.假设一个类的行为发生了变化并需要一个新的事件.您需要检查该类的每个用法.使用观察者界面,编译器将通知您需要注意的位置,但如果您使用事件,则会遇到更困难的时间. (13认同)
  • 我不同意您反对接口的观点,即接口使您实现接口上定义的每个方法/属性,因为这显然违反了接口隔离原则。拥有明确定义的单一用途接口不会导致您提到的这个问题 (3认同)
  • 接口的一个小专业人员是,当你有许多通常聚集在一起的事件时,你不必在一个巨大的事件+ = s中注册它们,而只注册那个接口.当然,我仍然喜欢活动:) (2认同)
  • 鲜为人知的事实;您实际上不必为事件声明委托类型。你可以只使用 `event Action` 或 `event Action<string,int>` 等。 (2认同)

Fre*_*els 29

嗯,事件可用于实现Observer模式.事实上,使用事件可以被视为观察者模式imho的另一种实现.

  • 绝对.这有点像问,"我应该实现迭代器模式还是使用foreach和IEnumerable?" (12认同)
  • 我知道这是观察者的模式; 我问我是否应该使用回调接口或使用事件来实现模式. (8认同)

ang*_*son 8

界面解决方案的优点:

  • 如果添加方法,现有观察者需要实现这些方法.这意味着您不太可能忘记将现有观察者连接到新功能.你当然可以将它们作为空方法来实现,这意味着你可以在某些"事件"的作用下仍然无所事事.但你不会那么容易忘记.
  • 如果使用显式实现,则还会以其他方式获得编译器错误,如果删除或更改现有接口,则实现它们的观察者将停止编译.

缺点:

  • 更多的想法必须进入规划,因为观察者界面的变化可能会在整个解决方案中强制执行更改,这可能需要不同的计划.由于简单事件是可选的,因此除非其他代码应对事件做出反应,否则很少或不需要更改其他代码.


Shu*_*oUk 7

事件的一些进一步好处.

  • 您可以免费获得适当的组播行为.
  • 如果您更改事件的订阅者以响应该事件,则行为已明确定义
  • 它们可以轻松而一致地进行内省(反映)
  • 事件的工具链支持(仅仅因为它们是.net中的成语)
  • 您可以选择使用它提供的异步apis

你可以自己实现所有这些(工具链除外),但这是非常困难的.例如:如果使用像List <>这样的成员变量来存储观察者列表.如果你使用foreach迭代它,那么任何在一个OnFoo()方法回调中添加或删除订阅者的尝试都将触发异常,除非你编写更多代码来干净地处理它.


Ale*_*sev 7

  • 事件很难通过对象链传播,例如,如果您使用FACADE模式或将工作委托给其他类.
  • 您需要非常小心地从事件中取消订阅以允许对象被垃圾收集.
  • 事件比简单函数调用慢2倍,如果对每次加注进行空检查,则慢3倍,并在null检查和调用之前复制事件委托以使其线程安全.

  • 另请阅读有关新(4.0)IObserver<T>界面的MSDN .

考虑这个例子:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


sna*_*arf 5

最好的决定方法是:哪一个更适合具体情况。这听起来可能是一个愚蠢或无益的答案,但我认为您不应该将其中一个视为“正确”的解决方案。

我们可以向您提供一百个提示。当观察者期望监听任意事件时,事件是最好的。当观察者希望列出所有给定的事件集时,界面是最好的。处理 GUI 应用程序时,事件是最好的选择。接口消耗更少的内存(多个事件的单个指针)。亚达亚达亚达。优点和缺点的项目符号列表值得考虑,但不是明确的答案。您真正需要做的是在实际应用中尝试它们并获得良好的感受。然后你可以选择更适合情况的一种。学习形式实践。

如果您必须使用单个定义问题,那么问问自己哪个更能描述您的情况:一组松散相关的事件,其中任何一个都可以使用或忽略,或者一组密切相关的事件,通常都需要由一名观察员。但是,我只是描述了事件模型和接口模型,所以我又回到了第一个问题:哪一个更适合这种情况?