观察者模式(Observer)
1.1.1 摘要
在系统的设计中,我们常常需要设计一个消息提示功能,让系统把提示信息发送到客户端。做到这一点的设计方案可以是多种多样,但是为了使系统能够易于复用,我们的设计应该遵守低耦合高内聚的设计原则,而且减少对象之间的耦合有利于系统的复用。观察者模式(Observer)是满足这一要求的各种设计方案中最重要的一种。
观察者模式(Observer):定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
1.1.2 正文
图1观察者(Observer)模式结构图
抽象主题(Subject)角色:主题角色把所有对观察考对象的引用保存在一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,主题角色又叫做抽象被观察者(Observable)角色,一般使用一个抽象类或者一个接口实现。
抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。这个接口叫做更新接口。抽象观察者角色一般用一个抽象类或者一个接口实现。在这个示意性的实现中,更新接口只包含一个方法(即Update()方法),这个方法叫做更新方法。
具体主题(ConcreteSubject)角色:将有关状态存入具体现察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者角色(Concrete Observable)。具体主题角色通常用一个具体子类实现。
具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体现察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。如果需要,具体现察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体子类实现。
观察者模式与推拉模式
推模式(Push):是一种基于客户器/服务器机制,由服务器主动将信息发送到客户器的技术。
拉模式(Pull)与推模式(Push)恰好相反,是由客户器主动向服务器发送请求的技术。
接下来让我们通过一段示例代码介绍观察者模式(Obsever),首先假设服务器状态更新后要同时通知客户端A,B,C直接的实现如下:
/// <summary> /// The Server class and when it updated, should notify the clients. /// </summary> public class Server { /// <summary> /// Keeps three objects referen. /// </summary> /// <param name="a">The object of ClentA.</param> /// <param name="b">The object of ClentB.</param> /// <param name="c">The object of ClentC.</param> public Server(ClientA a, ClientB b, ClientC c) { ClientA = a; ClientB = b; ClientC = c; } public ClientA ClientA { get; set; } public ClientB ClientB { get; set; } public ClientC ClientC { get; set; } public string SysMsg { get; set; } }
上面我们定义了服务器类,它包含了一个构造函数,通过它来保持ClientA,ClientB和ClientC类型对象的引用。
/// <summary> /// The client client, it would receive message /// when the sever updated. /// </summary> public class ClientA { /// <summary> /// Receives the message from sever. /// </summary> /// <param name="sysMsg"></param> public void Notify(string sysMsg) { SysMsg = sysMsg; } public string SysMsg { get; set; } } public class ClientB { public void Notify(string sysMsg) { SysMsg = sysMsg; } public string SysMsg { get; set; } } public class ClientC { public void Notify(string sysMsg) { SysMsg = sysMsg; } public string SysMsg { get; set; } }
接着我们定义了客户端类ClientA,ClientB和ClientC,由于它们是被动地接受信息(推模式),所有它们只包含接受信息的方法Notify()。
图2示例类图
上图我们知道Server类保持了ClientA,ClientB和ClientC类对象的引用。
static class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main() { var server = new Server( new ClientA(), new ClientB(), new ClientC()) { SysMsg = "Hi everyone" }; server.ClientA.Notify(server.SysMsg); server.ClientB.Notify(server.SysMsg); server.ClientC.Notify(server.SysMsg); } }
这里类型Server保持了ClientA,ClientB和ClientC类型的引用,但仔细考量一下如果我们要添加新的客户端类ClinetD,那么我们就需要修改Server的实现,这也意味着Server过度地依赖于客户端类,这严重地违反了面向对象设计的原则:面向接口编程,而非面向实现编程。其实ClientA,ClinetB和ClientC三个类型都有类似的功能——Notify()方法,所以我们可以为它们抽象出一个更高层的接口,通过封装变化使得我们设计依赖于抽象而非具体。
图3面向接口而非实现示例类图
现在Server类只依赖于接口IClent,如果我们需要添加新的客户端类,只需实现IClient接口就行了,而且Server不用做出任何修改。
现在我们对于为什么要使用观察者模式(Observer)有了初步的了解,接下来让我们通过更详细的例子介绍。
现在我们来实现观察者模式(Observer),假设Gof电信公司要把他们最新发明的设计模式介绍给每一位Geek,他们想通过服务器把信息发送到每位Geek的移动客户端,我们很荣幸协助他们完成这一项目,OK首先我们先创建抽象接口类:IObserver,然后我们创建具体观察者:MobilePhone,Tablet和Laptop,接着创建GofTelcom作为Subject。
首先我们定义接口IObserver,它只含一个Update()方法,由于提供了统一接口使得我们设计面对接口编程,而非面向实现编程。
/// <summary> /// The interface of observer. /// </summary> public interface IObserver { void Update(string msg); }
接着定义具体的观察者Mobile,Tablet和Laptop,它们都提供了Update()方法的具体实现。
/// <summary> /// Concrete observer. /// </summary> public class MobilePhone : IObserver { #region IObserver 成员 public void Update(string msg) { Console.WriteLine(String.Format("I am MobilePhone.\n New message: {0}", msg)); } #endregion } /// <summary> /// Concrete observer. /// </summary> public class Tablet : IObserver { #region IObserver 成员 public void Update(string msg) { Console.WriteLine(String.Format("OH...I am Tablet.\n New message: {0}", msg)); } #endregion } /// <summary> /// Concrete observer. /// </summary> public class Laptop : IObserver { #region Implementation of IObserver public void Update(string msg) { Console.WriteLine(String.Format("OH...I am Laptop.\n New message: {0}", msg)); } #endregion }
现在我们定义被观察者类GofTelecom,它定义了一个Notify()方法,委托GofNews()和事件GofNews。如果大家想参考什么是委托和事件可以参考《.NET 中的委托》和《.NET中的委托和事件(续)》。
/// <summary> /// The Subject class. /// </summary> public class GofTelecom { public delegate void GofNews(string msg); public static event GofNews NewsEvent; /// <summary> /// Notifies every obervers. /// </summary> /// <returns>If notification is successful return true, otherwise false.</returns> public static bool Notify() { if (NewsEvent != null) { NewsEvent(Message); return false; } return true; } public static String Message { get; set; } }
最后通过给GofTelecom对象添加MobilePhone,Tablet和Laptop对象来实现观察者和被观察者之间的关联。
static void Main(string[] args) { IList<IObserver> objObserver = new List<IObserver>(); // Registers observer. objObserver.Add(new MobilePhone()); objObserver.Add(new Tablet()); objObserver.Add(new Laptop()); // Attachs event and method. foreach (IObserver ob in objObserver) { GofTelecom.NewsEvent += ob.Update; } GofTelecom.Message = "Hi Everyone,/n We recommand you new Design Pattern."; Console.WriteLine(!GofTelecom.Notify() ? "Notify successful.\n" : "Notify failed.\n"); Console.ReadKey(); }
图4示例输出结果
在系统设计过程中,我们的系统设计基本上都要提供消息提示功能类似于QQ或MSN的消息提示框,而且这个功能恰好是观察者模式(Observer)实现的一种具体实现,在我们实现观察者模式(Observer)消息提示功能之前,先让我们回顾一下推拉模式吧!
推模式和拉模式的区别:
也许大家会好奇地问“使用推模式和拉模式,有什么区别呢?”
推模式的好处是能够及时响应,想要提供给Observer端什么数据,就将这些数据封装成对象,传递给Observer,缺点是需要创建自定义的事件关联信息,而且它必须继承于EventArgs对象。
缺点:精确性较差,不能保证能把信息送到客户器。
拉模式的好处则是不需要另外定义对象,直接将自身的引用传递进去就可以了。
缺点:不能够及时获取系统的变更。
.Net 中没有内置的IObserver和IObservable接口(Java中提供该接口类),我们可以通过委托和事件来完成,但是一样面临选择推模式还是拉模式的问题,何时使用哪种策略完全依赖于设计者,你也可以将两种方式都实现了,接下来我们的消息提示功能将通过拉模式实现。
首先我们通过给窗体控件添加属性TimerStatus,客户端通过Timer来发送消息请求到服务器端,TimerStatus是通过控件公开Timer属性的设置,具体实现如下:
/// <summary> /// The timer status, /// it includes base properties to set up timer. /// </summary> [TypeConverterAttribute(typeof(ClockStatusTypeConverter))] public class TimerStatus { private Timer _timer = new Timer(); private Frequencies _frequencies = Frequencies.Second; private int _interval = 23; private bool _start; /// <summary> /// Paramenterless constructor. /// </summary> public TimerStatus() : this(Frequencies.Minute, 23, true) { } public TimerStatus(Frequencies frequencies, int interval, bool start) { Frequencies = frequencies; Interval = interval; Start = start; } /// <summary> /// Gets or sets the frequencies. /// </summary> /// <value> /// The frequencies. /// </value> public Frequencies Frequencies { get { return _frequencies; } set { _frequencies = value; } } /// <summary> /// Gets or sets the interval. /// </summary> /// <value> /// The interval. /// </value> public int Interval { get { return _interval; } set { _interval = value; } } /// <summary> /// Gets or sets a value indicating /// whether this <see cref="TimerStatus"/> is start. /// </summary> /// <value> /// <c>true</c> if start; otherwise, <c>false</c>. /// </value> public bool Start { get { return _start; } set { _start = value; } } public Timer Timer { get { if (null == _timer) new Timer(); return _timer; } set { _timer = value; } } }
定义一个频率枚举,它提供Second,Minute和Hour单位来设置Timer的频率。
/// <summary> /// The frequency of timer. /// </summary> public enum Frequencies { Second, Minute, Hour }
现在我们完成了控件属性TimerStatus的定义,接着把该属性添加到我们自定义Form控件中,我们只需在自定义Form中添加属性如下:
/// <summary> /// Gets or sets the timer status. /// </summary> /// <value> /// The timer status. /// </value> [Browsable(true)] public TimerStatus TimerStatus { get { if (null == _timerStatus) return new TimerStatus(); return _timerStatus; } set { _timerStatus = value; } }
但我们发现自定义控件属性并不能编辑,这是由于控件没有办法把我们自定义类型TimeStatus转换为控件中实现的类型(如:字符串)。所以我们需要添加转换器类。
图5消息提示控件属性定义
/// <summary> /// Defines <see cref="TimerStatus"/> type converter. /// </summary> public class TimerStatusTypeConverter : ExpandableObjectConverter { /// <summary> /// Checks whether can convert from a string to <see cref="TimerStatus"/> or not. /// </summary> /// <param name="context"></param> /// <param name="sourceType">Source Type.</param> /// <returns>Can return true, otherwise false.</returns> public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { // Can convert from String to ClockStatus. if (sourceType == typeof(String)) return true; return base.CanConvertFrom(context, sourceType); } /// <summary> /// Checks whether can convert from <see cref="TimerStatus"/> to string or not. /// </summary> /// <param name="context"></param> /// <param name="destinationType">Destination Type</param> /// <returns>Can return true, otherwise false.</returns> public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(String)) return true; return base.CanConvertTo(context, destinationType); } /// <summary> /// Converts from stirng to <see cref="TimerStatus"/>. /// </summary> public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { var convertValue = value as string; if (String.IsNullOrEmpty(convertValue)) return new TimerStatus(); // Defines a enum convert. var enumConvert = TypeDescriptor.GetConverter(typeof(Frequencies)); // Get the string corresponding to control properties. var values = convertValue.Split(','); if (values.Length < 3) throw new ArgumentException("Wrong number of arguments"); int result1 = -1; bool result2; if (int.TryParse(values[1].ToString(), out result1) && bool.TryParse(values[2].ToString(), out result2)) return new TimerStatus( (Frequencies)enumConvert.ConvertFromString(values[0]), result1, result2); return base.ConvertFrom(context, culture, value); } /// <summary> /// Converts from <see cref="TimerStatus"/> stirng. /// </summary> public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { if (value is TimerStatus) { if (destinationType == typeof(String)) { var clockStatus = value as TimerStatus; return String.Format("{0},{1},{2}", clockStatus.Frequencies, clockStatus.Interval, clockStatus.Start); } } return base.ConvertTo(context, culture, value, destinationType); } }
上面我们定义了自定义类型转换器,它提供把TimerStatus类型转换为字符串的方法和通过属性参数创建TimerStatus对象的方法。
图6消息提示控件属性定义
现在我们定义好了消息提示框,而且我们可以通过属性浏览器直接编辑TimerStatus,如Frequencies设置Timer的频率,Interval时间间隔和Start是否启用Timer。
每个客户端通过拉模式访问服务器端,当有信息就返回到服务器端显示出来如下:
图7消息提示框设计
1.1.3 总结
观察者模式(Observer)的优点是实现了抽象与具体的分离,并定义了稳定的更新消息传递机制,类别清晰,并抽象了更新接口。
但是其缺点是每个观察者对象必须继承这个抽像出来的接口类,这样就造成了一些不方便,比如有一个别人写的观察者对象,并没有继承该抽象类,或者接口不对,我们又希望不修改该类直接使用它。虽然可以再应用Adapter模式来一定程度上解决这个问题,但是会造成更加复杂烦琐的设计,增加出错几率。
观察者模式(Observer)的效果有以下几个优点:
- 观察者模式在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所知道的只是一个具体现察者聚集,每一个具体现察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口。由于被观察者和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
- 观察者模式支持广播通信。被观察者会向所有的登记过的观察者发出通知。
观察者模式(Observer)有下面的一些缺点:
- 如果一个被观察者对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察考模式时要特别注意这一点。
- 如果对观察者的通知是通过另外的线程进行异步投递的话,系统必须保证投递是以自恰的方式进行的。
- 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎么发生变化的
参考
http://www.codeproject.com/KB/miscctrl/RobMisNotifyWindow.aspx
关于作者:[作者]:
JK_Rush从事.NET开发和热衷于开源高性能系统设计,通过博文交流和分享经验,欢迎转载,请保留原文地址,谢谢。 |