意图:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
适用性:
当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
模拟一个《奔跑吧兄弟》中 撕名牌 的游戏
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace DesignerModel.Observer { public abstract class IObserver { public abstract void Update(string msg); } /// <summary> /// 大广播 /// </summary> public class Subject { private List<Observer> observers = new List<Observer>(); public void Attach(Observer o) { observers.Add(o); } public void Detach(Observer o) { observers.Remove(o); } public void NotifyObservers(string msg) { foreach (var item in observers) { item.Update(msg); } } } /// <summary> /// 游戏者 /// </summary> public class Observer : IObserver { public Observer(string name) { Name = name; } public string Name { get; set; } public override void Update(string msg) { Console.WriteLine(this.Name + "收到信息:" + msg); } public string CreateNum() { RNGCryptoServiceProvider csp = new RNGCryptoServiceProvider(); byte[] byteCsp = new byte[10]; csp.GetBytes(byteCsp); return BitConverter.ToString(byteCsp); } } public class GameRule { private Subject Subject { get; set; } public GameRule(Subject subject) { Subject = subject; } /// <summary> /// 撕名牌 /// </summary> /// <param name="ob1"></param> /// <param name="ob2"></param> /// <returns></returns> public Observer RipTheNameplate(Observer ob1, Observer ob2) { Random rand1=new Random(1); Random rand2=new Random(5); int num1 = rand1.Next(1,1000); int num2 = rand2.Next(1, 1000); if (num1>num2) { Subject.NotifyObservers(ob1.Name+"Out"); return ob1; } else { Subject.NotifyObservers(ob2.Name + "Out"); return ob2; } } } public static class Client { public static void Run() { Subject sub = new Subject(); Observer Observer1 = new Observer("邓超"); Observer Observer2 = new Observer("郑凯"); Observer Observer3 = new Observer("Baby"); Observer Observer4 = new Observer("包贝尔"); Observer Observer5 = new Observer("陈赫"); Observer Observer6 = new Observer("李晨"); sub.Attach(Observer1); sub.Attach(Observer2); sub.Attach(Observer3); sub.Attach(Observer4); sub.Attach(Observer5); sub.Attach(Observer6); //让邓超和郑凯撕名牌 new GameRule(sub).RipTheNameplate(Observer1, Observer2); Console.Read(); } } }
目标和观察者之间的关系
按照模式的定义,目标和观察者之间是典型的1对多的关系,但是注意,如果观察者只有一个,也可以是1对1的 关系,这也使得在处理一个对象的状态变化会影响到另一个对象的时候,也可以考虑使用观察者模式。
同样的,一个观察者也可以观察多个目标,如果观察者为多个目标定义的通知更新方法都是Update方法的话,这会带来麻烦,因为需要接受多个目标的通知,如果是一个Update方法,那就需要在方法内部区分,到底这个更新的通知来自于哪一个目标,不同的目标有不同的后续操作。
一般情况下,观察者应该是不同的观察者目标定义不同的回调方法,这样实现最简单,不需要在update方法内部进行区分。
单向依赖,在观察者模式中,观察者和目标是单向依赖的,只有观察者依赖于目标,而目标是不依赖于观察者的。
他们之间的主动权掌握在目标手里,只有目标知道什么时候需要通知观察者,在整个过程中,观察者始终是被动的。被动的等待目标的通知。等待目标传值给它。
对目标而言,所有的观察者都是一样的,目标会一视同仁的对待。当然也可以通过在目标中进行控制,实现有区别的对待观察者。比如某些状态变化了,只需要通知部分观察者。但那是属于稍微变形的用法了。不属于标准的,原始的观察者模式。
基本的实现说明:
1,具体的目标实现对象要能维护观察者的注册信息,最简单的实现方案就如同前面的例子那样,采用一个集合来保存观察者的注册信息。
2,具体的目标实现对象需要维护引起通知的状态,一般情况下目标自身的变形使用的情况下,也可以是别的对象的状态。
3,具体的观察者实现对象需要能接受目标的通知,能够接受目标传递的数据,或者是能够主动去获取目标的数据,并进行后续处理
4,如果是一个观察者目标观察多个目标,那么在观察者的更新方法里,需要去判断是来自哪一个目标的通知。一种简单的解决方案就是扩展Update方法,比如在方法里面多传递一个参数进行区分等;还有一个更简单的方法,那就是干脆定义不同的回调方法。
命名建议
1,观察者模式又被称为 发布--订阅模式
2,目标接口的定义,建议在名称后面跟Subject
3,观察者接口的定义,建议在名称后面跟observer
观察者接口的更新方法,,建议名称为Update,当然方法的参数可以根据情况而定,参数个数,类型不限。
触发通知的时机:
在实现观察者模式的时候,一定要注意触发通知的时机,一般情况下,是在完成了状态维护后触发,因为通知会传递数据,不能够先通知后改数据,这很容易出问题,会导致观察者和目标对象的状态不一致。
比如,目标一触发通知,就有观察者来取值,结果目标还没有更新数据,这就明显地造成了错误。
观察者模式的两种模式:
推模式:目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,相当于是在广播通信
拉模式:目标对象在通知观察者的时候,只传少量信息。如果观察者需要更具体的信息。有观察者主动到目标对象中取,相当于是观察者从目标对象中拉数据。一般这种模式的实现中,会把目标对象自身通过update方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
推模式是假定目标对象知道观察者需要的数据,而拉模式是目标对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传给观察者,让观察者自己去按需求取值。
推模式可能会使得观察者对象难以复用,因为观察者定义的update方法是按需而定义的,可能无法兼顾没有考虑到的情况。这就意味着出现新情况的时候,就可能需要提供新的update方法,或者干脆重新实现观察者。而拉模式就不会造成这样的情况,因为拉模式下,update方法的参数就是目标对象本身,这基本上是目标对象能传递的最大数据集合了,基本上可以适应各种情况的需求。