65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中实现观察者模式(3 种技术)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (32投票s)

2014年7月13日

CPOL

9分钟阅读

viewsIcon

61601

downloadIcon

681

在 .NET 中实现观察者模式的多种技术

引言

在我们软件开发生涯的这些年里,我们都读过关于设计模式的知识。在所有模式中,我遇到的最酷、最实用的莫过于观察者模式,又称发布-订阅模式。你会发现许多关于如何使用框架中提供的程序集在 .NET Framework 中实现观察者模式的文章。但是,.NET Framework 已经发展了很多年,并且随着时间的推移,它也一直在提供创建观察者模式的新库。

例如,在 .NET 4.0 之前,我们必须编写自定义代码来实现观察者模式,或者使用委托和事件。随着框架 4.0 的发布,出现了非常酷的 IObservableIObserver 接口来实现观察者模式。在本文中,我们将通过代码示例介绍所有这些技术。

背景

为了让大家对观察者模式有一个简要的了解,它定义了对象之间(发布者和多个订阅者)的一对多依赖关系,因此当一个对象(发布者)改变状态时,所有依赖项(订阅者)都会自动收到通知并更新。

上述定义深受《Head First Design Patterns》一书的影响。我强烈建议任何开始学习设计模式的人阅读这本书。

示例场景

下面的代码基于一个场景,其中我们有一个气象站,负责记录天气数据(温度、湿度和气压)。

这些数据需要被多个显示器消费并相应地显示。每当气象站有新数据可用时,它应该将数据“推送”给显示器,所有显示器都应相应地更新。假设我们有 3 个显示器(当前状况、统计显示和天气预报显示),当气象站有新数据可用时,所有这些显示器都必须更新。

这个场景与《Head First Design Patterns》一书中提出的场景类似,但在这里,我们将讨论其 .NET 实现。

实现

因此,天气数据对象实际上是一个具有 3 个属性的对象:温度、湿度和气压。

WeatherData 类如下所示:

public class WeatherData
    {
        public float Temperature { get; set; }
        public float Humidity { get; set; }
        public float Pressure { get; set; }

        public WeatherData(float temp, float humid, float pres)
        {
            Temperature = temp;
            Humidity = humid;
            Pressure = pres;
        }
    }
}

现在,当气象站有新数据可用时,这些数据就需要被推送到天气显示器(订阅者)。

我们这里的代码示例将侧重于实现 .NET 4.0 之前的观察者模式和 .NET 4.0 之后的观察者模式。

每个部分的代码将分为发布者代码(类和接口)和订阅者代码(类和接口)。那么,我们开始吧。

观察者模式(.NET 4.0 之前)

<技术 #1>

使用纯面向对象(OO)编程概念

根据 OO 最佳实践,我们应该始终尝试面向接口编程,而不是面向实现。以下内容基于此。

出版社

这里的发布者实际上是一个天气数据提供者,负责提供天气数据。这个WeatherDataProvider类实现了IPublisher接口,如下所示:

public interface IPublisher
    {
        void RegisterSubscriber(ISubscriber subscriber);
        void RemoveSubscriber(ISubscriber subscriber);
        void NotifySubscribers();
    }

上面的三个方法执行以下操作:

  • RegisterSubscriber - 向发布者注册一个新订阅者。发布者必须将订阅者添加到它需要通知的订阅者列表中,以便在WeatherData发生更改时进行通知。
  • RemoveSubscriber - 从发布者的通知列表中删除一个已注册的订阅者
  • NotifySubscibers - 此方法实际上会调用订阅者对象上的一个方法,以通知其WeatherData已发生更改。

WeatherDataProvider的具体实现如下:

public class WeatherDataProvider : IPublisher
    {
        List<ISubscriber> ListOfSubscribers;
        WeatherData data;
        public WeatherDataProvider()
        {
            ListOfSubscribers = new List<ISubscriber>();
        }
        public void RegisterSubscriber(ISubscriber subscriber)
        {
            ListOfSubscribers.Add(subscriber);
        }

        public void RemoveSubscriber(ISubscriber subscriber)
        {
            ListOfSubscribers.Remove(subscriber);
        }

        public void NotifySubscribers()
        {
            foreach (var sub in ListOfSubscribers)
            {
                sub.Update(data);
            }
        }

        private void MeasurementsChanged()
        {
            NotifySubscribers();
        }
        public void SetMeasurements(float temp, float humid, float pres)
        {
            data = new WeatherData(temp, humid, pres);           
            MeasurementsChanged();
        }
    }

订阅者

这里的订阅者实际上是天气显示器,它们消费数据。每个订阅者都应该实现ISubscriber接口

public interface ISubscriber
    {
        void Update(WeatherData data);
    }

Subscriber接口只有一个方法;它显示从WeatherDataProvider接收到的当前WeatherData

CurrentConditionsDisplay的实现如下:

public class CurrentConditionsDisplay : ISubscriber
    {
        WeatherData data;
        IPublisher weatherData;

        public CurrentConditionsDisplay(IPublisher weatherDataProvider)
        {
            weatherData = weatherDataProvider;
            weatherData.RegisterSubscriber(this);
        }

        public void Display()
        {
            Console.WriteLine("Current Conditions : 
            Temp = {0}Deg | Humidity = {1}% | 
            Pressure = {2}bar", data.Temperature, data.Humidity, data.Pressure);
        }
       
        public void Update(WeatherData data)
        {
            this.data = data;
            Display();
        }
    }

在上面的代码中,我们进行了依赖注入或 IoC,通过构造函数注入了IPublisher接口。构造函数注入是 OO 编程中非常常见的做法。

因此,上面的代码发生的事情是:当显示器被实例化时,它会调用WeatherDataProviderRegisterSubscriber方法,并将自身注册为一个感兴趣的订阅者。

如果显示器想要取消注册,它必须调用WeatherDataProviderRemoveSubscriber方法。有多种方法可以实现这一点:你可以在析构函数(如上)中调用RemoveSubscriber,或者实现IDisposable并在Dispose方法中调用RemoveSubscriber,或者只是简单地创建一个类方法并在那里调用。

就像CurrentConditionsDisplay一样,我们也可以有一个ForecastDisplay(如下所示):

public class ForecastDisplay : ISubscriber, IDisposable
    {
        WeatherData data;
        IPublisher weatherData;

        public ForecastDisplay(IPublisher weatherDataProvider)
        {
            weatherData = weatherDataProvider;
            weatherData.RegisterSubscriber(this);
        }

        public void Display()
        {
            Console.WriteLine("Forecast Conditions : Temp = {0}Deg | 
                Humidity = {1}% | Pressure = {2}bar", data.Temperature + 6, 
                data.Humidity + 20, data.Pressure - 3);
        }

        public void Update(WeatherData data)
        {
            this.data = data;
            Display();
        }

        public void Dispose()
        {
            weatherData.RemoveSubscriber(this);
        }
    }

同样,你可以在这里定义任意数量的显示器,它们都可以订阅WeatherStation以获取更新。

为了演示上面的代码片段,我们将创建一个如下所示的示例控制台应用程序:

class Program
    {
        static void Main(string[] args)
        {
            WeatherDataProvider weatherData = new WeatherDataProvider();

            CurrentConditionsDisplay currentDisp = new CurrentConditionsDisplay(weatherData);            
            ForecastDisplay forecastDisp = new ForecastDisplay(weatherData);

            weatherData.SetMeasurements(40, 78, 3);
            Console.WriteLine();
            weatherData.SetMeasurements(45, 79, 4);
            Console.WriteLine();
            weatherData.SetMeasurements(46, 73, 6);

            //Remove forecast display
            forecastDisp.Dispose();
            Console.WriteLine();
            Console.WriteLine("Forecast Display removed");
            Console.WriteLine();
            weatherData.SetMeasurements(36, 53, 8);

            Console.Read();
        }
    }

输出应如下所示:

<技术 #2>

使用事件和委托

现在,转向在 .NET 中实现观察者模式的第二种方法。第一种方法当然没有利用任何 .NET 库来实现该模式。

在 .NET 中实现观察者模式时,此技术非常常用。它利用 .NET 框架中的事件和委托来实现。

我不会过多深入介绍以下源代码的细节,因为大部分内容都是不言自明的,但如果你需要有关以下实现的更多信息,可以参考 此 MSDN 链接

首先,此技术利用通用的EventHandler<T>委托来实现该模式。基本原理非常简单:

  • 发布者有一个public EventEventHandler,它在WeatherData每次更改时都会触发。
  • 订阅者附加到EventHandler,以便在事件触发时收到通知。
  • 在订阅者处收到的EventArgs e包含有关当前天气状况的数据。

首先,我们必须创建一个继承自EventArgsWeatherEventArgs类:

 public class WeatherDataEventArgs : EventArgs
    {
        public WeatherData data { get; private set; }
        public WeatherDataEventArgs(WeatherData data)
        {
            this.data = data;
        }
    }

此类包含将在事件处理程序中的事件参数中传递的天气数据。

出版社

这里的发布者是一个名为WeatherDataProvider的类,它在天气数据更改时触发事件,并通知已订阅的显示器:

public class WeatherDataProvider : IDisposable
    {
        public event EventHandler<WeatherDataEventArgs> RaiseWeatherDataChangedEvent;

        protected virtual void OnRaiseWeatherDataChangedEvent(WeatherDataEventArgs e)
        {
            // Make a temporary copy of the event to avoid possibility of
            // a race condition if the last subscriber unsubscribes
            // immediately after the null check and before the event is raised.
            EventHandler<WeatherDataEventArgs> handler = RaiseWeatherDataChangedEvent;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        public void NotifyDisplays(float temp, float humid, float pres)
        {
            OnRaiseWeatherDataChangedEvent
                 (new WeatherDataEventArgs(new WeatherData(temp, humid, pres)));
        }

        public void Dispose()
        {
            if (RaiseWeatherDataChangedEvent != null)
            {
                foreach (EventHandler<WeatherDataEventArgs> 
                item in RaiseWeatherDataChangedEvent.GetInvocationList())
                {
                    RaiseWeatherDataChangedEvent -= item;
                }
            }
        }
    }

上面的代码声明了一个通用类型的EventHandler委托,该委托接受WeatherDataEventArgs作为要提供给事件处理程序的参数。

每次调用NotifyDisplays方法时,都会触发OnRaiseWeatherDataChangedEvent,该方法又会在以下代码的相应显示器(订阅者)中调用事件处理程序。

订阅者

CurrentConditionsDisplay的实现如下。我们可以有任意数量的显示器附加到Event,通过Event处理程序。

 public class CurrentConditionsDisplay
    {
        WeatherData data;
        WeatherDataProvider WDprovider;

        public CurrentConditionsDisplay(WeatherDataProvider provider)
        {
            WDprovider = provider;
            WDprovider.RaiseWeatherDataChangedEvent += provider_RaiseWeatherDataChangedEvent;
        }

        void provider_RaiseWeatherDataChangedEvent(object sender, WeatherDataEventArgs e)
        {
            data = e.data;
            UpdateDisplay();
        }

        public void UpdateDisplay()
        {
            Console.WriteLine("Current Conditions : Temp = {0}Deg | 
            Humidity = {1}% | Pressure = {2}bar", data.Temperature, data.Humidity, data.Pressure);
        }

        public void Unsubscribe()
        {
            WDprovider.RaiseWeatherDataChangedEvent -= provider_RaiseWeatherDataChangedEvent;
        }
    }

像上面一样,我们也可以有一个ForecastDisplay(在源代码中提供)。

可以使用如下代码的控制台应用程序来演示上述用法的示例:

class Program
    {
        static void Main(string[] args)
        {
            WeatherDataProvider provider = new WeatherDataProvider();

            CurrentConditionsDisplay current = new CurrentConditionsDisplay(provider);            
            ForecastDisplay forecast = new ForecastDisplay(provider);

            provider.NotifyDisplays(40, 78, 3);
            Console.WriteLine();
            provider.NotifyDisplays(42, 68, 5);
            Console.WriteLine();
            provider.NotifyDisplays(45, 68, 8);
            Console.WriteLine();
            forecast.Unsubscribe();
            Console.WriteLine("Forecast Display removed");
            Console.WriteLine();
            provider.NotifyDisplays(30, 58, 1);

            //Code to call to detach all event handler
            provider.Dispose();

            Console.Read();
        }
    }

观察者模式(.NET 4.0 及更高版本)

当 .NET 框架发布时,它带来了 C# 许多备受期待的语言特性,如动态类型、可选参数、IObservable 等。其中一些特性在其他语言/框架中已经可用,但在 C# 中却不可用。

其中最值得注意的之一是IObservable<T>IObserver<T>。这些在 Java 中就已经可用,直到后来才引入 C#,它们确实是我最喜欢的功能之一。这些集合为我们开发者带来了一些独特而巧妙的功能供我们利用。其中最重要的是能够实现健壮的发布-订阅模型,即观察者模式。

以下实现与 此处 MSDN 链接中的示例实现一致。

<技术 #3>

使用 IObservable<T> 和 IObserver<T>

这些库使得实现观察者模式变得最容易。事实上,它非常简单,以至于在使用这些库时,你可能不会意识到自己正在使用程序中的发布-订阅模型。

在通用类型IObservable<T>IObserver<T>中,我们这里的T将是WeatherData

出版社

发布者(WeatherDataProvider)只需要实现IObservable<T>接口(如下所示)。

public class WeatherDataProvider : IObservable<WeatherData>
    {
        List<IObserver<WeatherData>> observers;

        public WeatherDataProvider()
        {
            observers = new List<IObserver<WeatherData>>();
        }

        public IDisposable Subscribe(IObserver<WeatherData> observer)
        {
            if (!observers.Contains(observer))
            {
                observers.Add(observer);
            }
            return new UnSubscriber(observers, observer);
        }

        private class UnSubscriber : IDisposable
        {
            private List<IObserver<WeatherData>> lstObservers;
            private IObserver<WeatherData> observer;

            public UnSubscriber(List<IObserver<WeatherData>> ObserversCollection, 
                                IObserver<WeatherData> observer)
            {
                this.lstObservers = ObserversCollection;
                this.observer = observer;
            }

            public void Dispose()
            {
                if (this.observer != null)
                {
                    lstObservers.Remove(this.observer);
                }
            }
        }

        private void MeasurementsChanged(float temp, float humid, float pres)
        {
            foreach (var obs in observers)
            {
                obs.OnNext(new WeatherData(temp, humid, pres));
            }
        }

        public void SetMeasurements(float temp, float humid, float pres)
        {
            MeasurementsChanged(temp, humid, pres);
        }
    }

观察者通过调用其IObservable<T>.Subscribe方法来注册接收通知,该方法将observer对象的引用分配给一个private的通用List<T>对象。该方法返回一个Unsubscriber对象,它是一个IDisposable实现,允许观察者停止接收通知。Unsubscriber类是一个简单的嵌套类,它实现了IDisposable,并且还维护了一个已订阅用户的列表,并由观察者(在本例中为Displays)用于取消订阅。我们将在下一节中了解更多关于这方面的信息。

此外,还有一个我们已调用OnNext函数在订阅者/观察者上。这个函数实际上是IObserver的内置函数,表示集合中发生了变化。这实际上是通知订阅者变化的函数。

除了OnNext之外,还有OnErrorOnCompleted函数。我们将在下一节讨论所有这些。

订阅者

订阅者(Displays)只需要实现IObserver<T>接口CurrentConditionsDisplay的实现如下。我们可以类似地有任意数量的显示器(请参见源代码)。

public class CurrentConditionsDisplay : IObserver<WeatherData>
    {
        WeatherData data;
        private IDisposable unsubscriber;

        public CurrentConditionsDisplay()
        {

        }
        public CurrentConditionsDisplay(IObservable<WeatherData> provider)
        {
            unsubscriber = provider.Subscribe(this);
        }
        public void Display()
        {
            Console.WriteLine("Current Conditions : Temp = {0}Deg | 
            Humidity = {1}% | Pressure = {2}bar", data.Temperature, data.Humidity, data.Pressure);
        }

        public void Subscribe(IObservable<WeatherData> provider)
        {
            if (unsubscriber == null)
            {
                unsubscriber = provider.Subscribe(this);
            }
        }

        public void Unsubscribe()
        {
            unsubscriber.Dispose();
        }

        public void OnCompleted()
        {
            Console.WriteLine("Additional temperature data will not be transmitted.");
        }

        public void OnError(Exception error)
        {
            Console.WriteLine("Some error has occurred..");
        }

        public void OnNext(WeatherData value)
        {
            this.data = value;
            Display();
        }
    }

上面代码中值得关注的点如下:

  • 当你调用subscriber方法时,它会返回一个实现IDisposable的对象(在本例中为Unsubscriber)。因此,当我们调用该对象上的Dispose时,它会自动调用Unsubscribe
  • 发布者/提供者可以通过以下 3 种方法调用订阅者/观察者:
    • IObserver<T>.OnNext方法,将一个包含当前数据、更改数据或新数据的T对象传递给观察者。
    • IObserver<T>.OnError方法,通知观察者发生了某个错误条件(请注意,当提供者中发生异常时,这不会自动调用。程序员的责任是捕获提供者中的异常,然后调用此函数)。
    • IObserver<T>.OnCompleted方法,通知观察者它已完成发送通知。

可以使用如下代码的控制台应用程序来创建演示此代码的演示程序:

 class Program
    {
        static void Main(string[] args)
        {
            WeatherDataProvider weatherDataO = new WeatherDataProvider();

            CurrentConditionsDisplay currentDisp = new CurrentConditionsDisplay(weatherDataO);

            ForecastDisplay forecastDisp = new ForecastDisplay(weatherDataO);

            weatherDataO.SetMeasurements(40, 78, 3);
            Console.WriteLine();
            weatherDataO.SetMeasurements(45, 79, 4);
            Console.WriteLine();
            weatherDataO.SetMeasurements(46, 73, 6);
            //Remove forecast display
            forecastDisp.Unsubscribe();

            Console.WriteLine();
            Console.WriteLine("Forecast Display removed");
            Console.WriteLine();
            weatherDataO.SetMeasurements(36, 53, 8);

            Console.Read();
        }
    }

关注点

当我开始学习设计模式,特别是观察者模式时,我发现可以在 .NET Framework 中以多种方式实现它。

经过一番研究,我平息了我的好奇心,并确定了上述 3 种实现技术。当然,你可能还有其他很棒的观察者模式实现方式,我很乐意了解。请在下方的评论中告诉我。

历史

© . All rights reserved.