【问题标题】:C#: events or an observer interface? Pros/cons?C#:事件还是观察者接口?优点缺点?
【发布时间】:2010-10-07 17:43:31
【问题描述】:

我有以下(简化):

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

class FindFiles
{
    IFindFilesObserver _observer;

    // ...
}

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

事件与传统观察者界面相比有哪些优点或缺点?

【问题讨论】:

    标签: c# events observer-pattern


    【解决方案1】:

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

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

    更少的维护
    关于事件的另一个好处是您可以向特定类添加一个新的,以便它会引发它,并且您不必更改每个现有的观察者。然而,如果你想给一个接口添加一个新方法,你必须遍历每个已经实现该接口的类,并在所有类中实现这个新方法。但是,对于一个事件,您只需要更改现有的类,这些类实际上想要做一些事情来响应您添加的新事件。

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

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

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

    【讨论】:

    • 接口的一个小优点是,当您有很多通常聚集在一起的事件时,您不必将它们全部注册在一大堆事件 +=s 中,而只需注册那个界面。当然,我仍然更喜欢活动 :)
    • 事件的一个缺点是它们不会跨应用程序域触发。如果您的应用(或将要)复杂,这将成为一个问题。
    • 事件对于框架来说很有用,因为它们通常不会经常变化,当人们开始使用它们时,它们应该相对稳定。在开发具有快速变化的规范的应用程序时,情况可能并非如此。假设一个类的行为发生变化并需要一个新事件。您需要检查该类的每一种用法。使用观察者界面,编译器会通知你需要注意的地方,但如果你使用事件,你会遇到更困难的时间。
    • 鲜为人知的事实;您实际上不必为事件声明委托类型。您可以使用event Actionevent Action<string,int> 等。
    • 我不同意您反对接口的观点,即接口使您实现接口上定义的每个方法/道具,因为这显然违反了接口隔离原则。定义明确的单一用途接口不会导致您提到的这个确切问题
    【解决方案2】:

    嗯,事件可以用来实现观察者模式。事实上,使用事件可以看作是观察者模式恕我直言的另一种实现。

    【讨论】:

    • 当然。这有点像在问,“我应该实现迭代器模式还是使用 foreach 和 IEnumerable?”
    • 观察者触发的事件是通知多个消费者应用程序发生变化的一种自然方式
    • 我知道这是观察者模式;我问我应该使用回调接口还是使用事件来实现该模式。
    • 我相信使用事件通常是一个更好的选择,因为默认情况下它可以解耦两个组件,并间接支持将来拥有多个事件源而无需更改任何代码。
    【解决方案3】:
    • 事件很难通过对象链传播,例如,如果您使用 FACADE 模式或将工作委托给其他类。
    • 您需要非常小心地取消订阅事件以允许对对象进行垃圾收集。
    • 事件比简单的函数调用慢 2 倍,如果您在每次引发时执行空检查,则慢 3 倍,并在空检查和调用之前复制事件委托以使其线程安全。

    • 还有read MSDN about new (in 4.0) IObserver<T> interface.

    考虑这个例子:

    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();
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      接口解决方案的优点:

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

      缺点:

      • 在规划方面需要考虑更多,因为观察者界面的更改可能会强制更改整个解决方案,这可能需要不同的规划。由于一个简单的事件是可选的,因此除非其他代码应对该事件做出反应,否则很少或不需要更改其他代码。

      【讨论】:

        【解决方案5】:

        活动的一些进一步好处。

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

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

        【讨论】:

        • 它提供了哪些异步api?
        【解决方案6】:

        优点是事件更“点网”。如果您正在设计可以拖放到表单上的非可视组件,则可以使用设计器将它们连接起来。

        缺点是事件仅表示单个事件 - 您需要为每个要通知观察者的“事物”设置一个单独的事件。这实际上并没有太大的实际影响,除了每个观察到的对象都需要为每个事件的每个观察者保存一个引用,在有很多观察到的对象的情况下会膨胀内存(这是他们做出不同方式的原因之一)管理 WPF 中的观察者/可观察关系)。

        在您的情况下,我认为这没有太大区别。如果观察者通常对所有这些事件感兴趣,请使用观察者界面而不是单独的事件。

        【讨论】:

        • 好吧,没有什么能阻止你拥有一个通过 EventArgs 的子类区分不同事件的单个 c# 事件 - 不过我不会这样做。
        【解决方案7】:

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

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

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

        【讨论】:

        • 这就是我要求优缺点的原因。我意识到没有“一刀切”的解决方案。这只是我正在权衡的事情,并认为这将是一个有趣的问题。
        【解决方案8】:

        我更喜欢基于事件的解决方案,原因如下

        • 它降低了进入成本。说“+= new EventHandler”比实现一个完整的接口要容易得多。
        • 它降低了维护成本。如果您将新事件添加到您的课程中,这就是您需要做的所有事情。如果向接口添加新事件,则必须更新代码库中的每个消费者。或者定义一个全新的接口,随着时间的推移,消费者会感到厌烦“我是实现 IRandomEvent2 还是 IRandomEvent5?”
        • 事件允许处理程序不基于类(即某处的静态方法)。没有强制所有事件处理程序成为实例成员的功能原因
        • 将一堆事件分组到一个界面中是对事件的使用方式做出假设(仅此而已,假设)
        • 与原始事件相比,接口没有真正的优势。

        【讨论】:

          【解决方案9】:

          Java 对匿名接口有语言支持,所以在 Java 中使用回调接口。

          C# 支持匿名委托 - lambdas - 因此事件是在 C# 中使用的东西。

          【讨论】:

            【解决方案10】:

            接口的一个好处是它们更容易应用装饰器。标准示例:

            subject.RegisterObserver(new LoggingObserver(myRealObserver));
            

            相比:

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

            (我是装饰器模式的忠实粉丝)。

            【讨论】:

            • 在界面上应用装饰器模式同样容易。如果所有输入都成为问题,只需在 IDE 中使用生产力工具即可。
            【解决方案11】:

            如果您的对象需要以某种保留引用的方式进行序列化,例如 NetDataContractSerializerprotobuf 事件将无法跨越序列化边界。由于观察者模式仅依赖于对象引用,因此如果需要,它可以毫无问题地使用这种类型的序列化。

            例如。您有一堆相互链接的业务对象,您需要将它们传递给 Web 服务。

            【讨论】:

              猜你喜欢
              • 2016-05-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多