https://www.cnblogs.com/abcdwxc/archive/2007/09/19/898856.html 本文详细讲述了观察者模式从头开始的起源,到第一次接口解耦,到都是用接口解耦,到推模式只能获取本对象,引出拉模式,到使用.net下的事件最终解耦!
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
适用性:
1.当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
2.当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
3.当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
这种模式与事件(event)很像,杰杰借鉴于博主的文章,尝试更加简明的书写观察者模式中的奥秘
例子仍然使用如下:
生活中的例子:
观 察者定义了对象间一对多的关系,当一个对象的状态变化时,所有依赖它的对象都得到通知并且自动地更新。在ATM取款,当取款成功后,以手机、邮件等方式进行通知。
代码实现:
如果只考虑二者之间的关系(取钱的时候通知邮件和电话的话),实现的代码如下:
#region 银行账户类 public class BankAccount { private Email _email; private Mobile _mobile; private double _money; public BankAccount(double money) { this._money = money; } public Email Email { get { return _email; } set { _email = value; } } public Mobile Mobile { get { return _mobile; } set { _mobile = value; } } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { _email.SendEmail(this); _mobile.SendMessage(this); } } #endregion
#region 通知类 public class Email { private string _email; public Email(string email) { this._email = email; } public void SendEmail(BankAccount bankaccount) { Console.WriteLine("向{0}发送邮件,取出金额为{1}",_email, bankaccount.Money); } } public class Mobile { public string _phoneNumer; public Mobile(string phoneNumer) { this._phoneNumer = phoneNumer; } public void SendMessage(BankAccount bankaccount) { Console.WriteLine("向{0}打电话通知,取出金额为{1}", _phoneNumer, bankaccount.Money); } } #endregion
static void Main(string[] args) { BankAccount bankaccount = new BankAccount(30000d); Email email = new Email("12345@qq.com"); Mobile mobile = new Mobile("0411-12345678"); bankaccount.Email = email; bankaccount.Mobile = mobile; bankaccount.WithDraw(); Console.ReadKey(); }
=====================***********************=====================***********************=====================
显然这种方式书写的代码考虑的太狭窄,此时通知与取钱账户之间形成了紧耦合,也就是说,无法扩展取钱去通知其他账户,除非修改账户类本身。
显然这样的设计极大的违背了“开放-封闭”原则,这不是我们所想要的,仅仅是新增加了一种通知对象,就需要对原有的BankAccount类进行修改,这样的设计是很糟糕的。对此做进一步的抽象,既然出现了多个通知对象,我们就为这些对象之间抽象出一个接口,用它来取消BankAccount和具体的通知对象之间依赖。
由此我们由左图转换到右图。
我只需要为所有通知类增加一个公共接口,让所有的通知类实现这个接口即可
#region 通知接口 public interface IObserverAccount { void Update(BankAccount bankaccount); } #endregion
#region 通知类 public class Email: IObserverAccount { private string _email; public Email(string email) { this._email = email; } public void Update(BankAccount bankaccount) { Console.WriteLine("向{0}发送邮件,取出金额为{1}",_email, bankaccount.Money); } } public class Mobile: IObserverAccount { public string _phoneNumer; public Mobile(string phoneNumer) { this._phoneNumer = phoneNumer; } public void Update(BankAccount bankaccount) { Console.WriteLine("向{0}打电话通知,取出金额为{1}", _phoneNumer, bankaccount.Money); } } #endregion
对于银行账户类只需要将具体的实现类,换成接口即可,实现面向接口的编程
#region 银行账户类 public class BankAccount { private IObserverAccount _email; private IObserverAccount _mobile; private double _money; public BankAccount(double money) { this._money = money; } public IObserverAccount Email { get { return _email; } set { _email = value; } } public IObserverAccount Mobile { get { return _mobile; } set { _mobile = value; } } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { _email.Update(this); _mobile.Update(this); } } #endregion
对于实现与上方一致,将具体的类转换为接口就可以(转不转都行,因为传递的时候,就算不转,程序也会帮我们隐示转换)
static void Main(string[] args) { BankAccount bankaccount = new BankAccount(30000d); IObserverAccount email = new Email("12345@qq.com"); IObserverAccount mobile = new Mobile("0411-12345678"); bankaccount.Email = email; bankaccount.Mobile = mobile; bankaccount.WithDraw(); Console.ReadKey(); }
结果也是相同的。
=====================***********************=====================***********************=====================
现在出现的问题是账户类中固定了只能选两种通知方式,无法手动增减,因此考虑,将账户中的通知方式变为接口列表
此时账户类修改为:
#region 银行账户类 public class BankAccount { private List<IObserverAccount> list = new List<IObserverAccount>(); private double _money; public BankAccount(double money) { this._money = money; } public void AddObserver(IObserverAccount iobserveraccount) { list.Add(iobserveraccount); } public void RemoveObserver(IObserverAccount iobserveraccount) { list.Remove(iobserveraccount); } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { foreach (IObserverAccount item in list) { item.Update(this); } } } #endregion
实现方式微调如下:
static void Main(string[] args) { BankAccount bankaccount = new BankAccount(30000d); IObserverAccount email = new Email("12345@qq.com"); IObserverAccount mobile = new Mobile("0411-12345678"); bankaccount.AddObserver(email); bankaccount.AddObserver(mobile); //bankaccount.Email = email; //bankaccount.Mobile = mobile; bankaccount.WithDraw(); Console.ReadKey(); }
=====================***********************=====================***********************=====================
走到这一步,已经有了Observer模式的影子了,BankAccount类不再依赖于具体的Emailer或Mobile,而是依赖于抽象的IObserverAccount。存在着的一个问题是Emailer或Mobile仍然依赖于具体的BankAccount,解决这样的问题很简单,只需要再对BankAccount类做一次抽象。如下图:
我们设置一个账户的抽象基类(所有具体的账户类都要实现这个接口):
#region 银行账户抽象基类 public abstract class Subject { private List<IObserverAccount> list = new List<IObserverAccount>(); private double _money; public Subject(double money) { this._money = money; } public void AddObserver(IObserverAccount iobserveraccount) { list.Add(iobserveraccount); } public void RemoveObserver(IObserverAccount iobserveraccount) { list.Remove(iobserveraccount); } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { foreach (IObserverAccount item in list) { item.Update(this); } } } #endregion
#region 银行账户类 public class BankAccount : Subject { public BankAccount(double money) : base(money) { } } #endregion
将通知类实现具体账户的参数,全部修改为该接口参数,实现面向接口编程
#region 通知接口 public interface IObserverAccount { void Update(Subject subject); } #endregion #region 通知类 public class Email: IObserverAccount { private string _email; public Email(string email) { this._email = email; } public void Update(Subject subject) { Console.WriteLine("向{0}发送邮件,取出金额为{1}",_email, subject.Money); } } public class Mobile: IObserverAccount { public string _phoneNumer; public Mobile(string phoneNumer) { this._phoneNumer = phoneNumer; } public void Update(Subject subject) { Console.WriteLine("向{0}打电话通知,取出金额为{1}", _phoneNumer, subject.Money); } } #endregion
具体实现代码仍然不变
static void Main(string[] args) { BankAccount bankaccount = new BankAccount(30000d); IObserverAccount email = new Email("12345@qq.com"); IObserverAccount mobile = new Mobile("0411-12345678"); bankaccount.AddObserver(email); bankaccount.AddObserver(mobile); //bankaccount.Email = email; //bankaccount.Mobile = mobile; bankaccount.WithDraw(); Console.ReadKey(); }
=====================***********************=====================***********************=====================
推模式与拉模式
对于发布-订阅模型,大家都很容易能想到推模式与拉模式,用SQL Server做过数据库复制的朋友对这一点很清楚。在Observer模式中同样区分推模式和拉模式,我先简单的解释一下两者的区别:推模式是当有消息时,把消息信息以参数的形式传递(推)给所有观察者,而拉模式是当有消息时,通知消息的方法本身并不带任何的参数,是由观察者自己到主体对象那儿取回(拉)消息。知道了这一点,大家可能很容易发现上面我所举的例子其实是一种推模式的Observer模式。我们先看看这种模式带来了什么好处:当有消息时,所有的 观察者都会直接得到全部的消息,并进行相应的处理程序,与主体对象没什么关系,两者之间的关系是一种松散耦合。但是它也有缺陷,第一是所有的观察者得到的 消息是一样的,也许有些信息对某个观察者来说根本就用不上,也就是观察者不能“按需所取”;第二,当通知消息的参数有变化时,所有的观察者对象都要变化。鉴于以上问题,拉模式就应运而生了,它是由观察者自己主动去取消息,需要什么信息,就可以取什么,不会像推模式那样得到所有的消息参数。
(简要就是推模式的通知者无法选择需要接收对象的信息(也无法控制什么时候接收),而拉模式,可以选择去接收哪个对象的信息,以及接收哪些信息,而不是接收全部信息)
拉模式实现如下:
银行账户方不变化,将通知类方,从接口开始去除指定的银行账户基类,将账户基类由各个具体的通知方,在构造函数时指定
//该块不变 #region 银行账户抽象基类 public abstract class Subject { private List<IObserverAccount> list = new List<IObserverAccount>(); private double _money; public Subject(double money) { this._money = money; } public void AddObserver(IObserverAccount iobserveraccount) { list.Add(iobserveraccount); } public void RemoveObserver(IObserverAccount iobserveraccount) { list.Remove(iobserveraccount); } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { foreach (IObserverAccount item in list) { item.Update(); } } } #endregion #region 银行账户类 public class BankAccount : Subject { public BankAccount(double money) : base(money) { } }
#region 通知接口 public interface IObserverAccount { void Update(); } #endregion #region 通知类 public class Email: IObserverAccount { private string _email; private Subject _subject; public Email(string email, Subject subject) { this._email = email; this._subject = subject; } public void Update() { Console.WriteLine("向{0}发送邮件,取出金额为{1}",_email, _subject.Money); } } public class Mobile: IObserverAccount { public string _phoneNumer; private Subject _subject; public Mobile(string phoneNumer,Subject subject) { this._phoneNumer = phoneNumer; this._subject = subject; } public void Update() { Console.WriteLine("向{0}打电话通知,取出金额为{1}", _phoneNumer, _subject.Money); } } #endregion
static void Main(string[] args) { Subject bankaccount = new BankAccount(30000d); Subject bankaccount2 = new BankAccount(20000d); IObserverAccount email = new Email("12345@qq.com", bankaccount); IObserverAccount mobile = new Mobile("0411-12345678", bankaccount2); bankaccount.AddObserver(email); bankaccount.AddObserver(mobile); bankaccount.WithDraw(); Console.ReadKey(); }
=====================***********************=====================***********************=====================
正如最上方提到的在C#中有事件这个概念,它本身就是一种观察者模式的实现方式,借助于它,我们可以构建出更加完善的观察者模式!
在银行账户类中添加一个事件,事件可以动态绑定,这样也不需要list集合了
#region 银行账户抽象基类 public abstract class Subject { public event Action<object> NotifyEvent; private double _money; public Subject(double money) { this._money = money; } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { if (NotifyEvent != null) { NotifyEvent(this); } } } #endregion
#region 银行账户类 public class BankAccount : Subject { public BankAccount(double money) : base(money) { } }
我将通知接口写成具有泛型的形式,这样以后在账户的具体类中添加属性等,我仍然可以访问的到
#region 通知接口 public interface IObserverAccount { void Update<T>(object obj) where T : Subject; }
#region 通知类 public class Email : IObserverAccount { private string _email; public Email(string email) { this._email = email; } public void Update<T>(object obj) where T: Subject { try { T t = (T)obj; Console.WriteLine("向{0}发送邮件,取出金额为{1}", _email, t.Money); } catch { } } }
static void Main(string[] args) { Subject bankaccount = new BankAccount(30000d); IObserverAccount email = new Email("12345@qq.com"); bankaccount.NotifyEvent += email.Update<BankAccount>; bankaccount.WithDraw(); Console.ReadKey(); }
Observer实现要点:
1.使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达到松耦合。
2.目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。观察者自己决定是否需要订阅通知。目标对象对此一无所知。
3.在C#中的Event。委托充当了抽象的Observer接口,而提供事件的对象充当了目标对象,委托是比抽象Observer接口更为松耦合的设计。
虽然我们使用event实现的属于推模式,但是这种模式更加常用,基本上需求都可以满足 ,最终的需求也都可以实现了。
=====================***********************=====================***********************=====================
杰杰继续思索,如果将通知类的接口,扩展成通知的抽象基类的话,在其中加入list,记录所有的通知类对象,统一绑定到指定的账户类的事件上,岂不是一次就可以完成
//银行账户类保持不变 #region 银行账户抽象基类 public abstract class Subject { public event Action<object> NotifyEvent; private double _money; public Subject(double money) { this._money = money; } public double Money { get { return _money; } set { _money = value; } } public void WithDraw() { if (NotifyEvent != null) { NotifyEvent(this); } } } #endregion #region 银行账户类 public class BankAccount : Subject { public BankAccount(double money) : base(money) { } } #endregion
#region 通知基类 public abstract class ObserverAccountClass { public ObserverAccountClass() { list.Add(this);//将自己绑定到list中 } public abstract void Update<T>(object obj) where T : Subject; List<ObserverAccountClass> list = new List<ObserverAccountClass>(); public void Add(ObserverAccountClass observeraccountclass) { list.Add(observeraccountclass); } public void Remove(ObserverAccountClass observeraccountclass) { list.Remove(observeraccountclass); } public void BindingEvents(Subject subject) { foreach (ObserverAccountClass item in list) { subject.NotifyEvent += item.Update<Subject>; } } } #endregion
将具体通知类继承它
#region 通知类 public class Email : ObserverAccountClass { private string _email; public Email(string email) { this._email = email; } public override void Update<T>(object obj) { try { T t = (T)obj; Console.WriteLine("向{0}发送邮件,取出金额为{1}", _email, t.Money); } catch { } } }
static void Main(string[] args) { Subject bankaccount = new BankAccount(30000d); ObserverAccountClass email = new Email("12345@qq.com"); ObserverAccountClass email2 = new Email("22335@qq.com"); email.Add(email2); email.BindingEvents(bankaccount); //bankaccount.NotifyEvent += email.Update<BankAccount>; bankaccount.WithDraw(); Console.ReadKey(); }
(写的行数其实与不使用list是一样的,只不过可以通过另外一种形式一起绑定)
其实可扩展的东西有很多,自己慢慢发掘,体会观察者模式的好处吧!
最后强调一点,其实讲了那么多的过程,最最基本,也是最最重要的就是event,个人理解这是C#自带的观察者模式,十分强大!