设计模式-观察者模式上
观察者模式可以说是非常贴近我们生活的一个设计模式,为什么这么说呢?哲学上有这么一种说法,叫做“万事万物皆有联系”,原意是说世上没有孤立存在的事物,但其实也可以理解为任何一个事件的发生必然由某个前置事件引起,也必然会导致另一个后置事件。我们的生活中,充斥着各种各样的相互联系的事件,而观察者模式,主要就是用于处理这种事件的一套解决方案。
示例
观察者模式在不同需求下,实现方式也不尽相同,我们还是举一个例子,然后通过逐步的改进来深刻感受一下它是如何工作的。
在中学阶段有一篇课文《口技》,其中有一句“遥闻深巷中犬吠,便有妇人惊觉欠伸,其夫呓语。既而儿醒,大啼。”应该不用翻译吧?我们接下来就是要通过程序模拟一下这个场景。
先看看他们之间的关系,如下图所示:
初版实现
一声狗叫引发了一系列的事件,需求很清晰,也很简单。于是,我们可以很容易的得到如下实现:
public class Wife
{
public void Wakeup()
{
Console.WriteLine("便有妇人惊觉欠伸");
}
}
public class Husband
{
public void DreamTalk()
{
Console.WriteLine("其夫呓语");
}
}
public class Son
{
public void Wakeup()
{
Console.WriteLine("既而儿醒,大啼");
}
}
public class Dog
{
private readonly Wife _wife = new Wife();
private readonly Husband _husband = new Husband();
private readonly Son _son = new Son();
public void Bark()
{
Console.WriteLine("遥闻深巷中犬吠");
_wife.Wakeup();
_husband.DreamTalk();
_son.Wakeup();
}
}
功能实现了,调用很简单,就不上代码了,从Dog
类中可以看出,确实是狗叫触发了后续的一系列事件。但是,有一定经验的人一定很快就会发现,这里至少违反了开闭原则和迪米特原则,最终会导致扩展维护起来比较麻烦。因此,需要改进,而改进的方法也不难想到,无非就是抽象出一个基类或接口,让面向实现编程的部分变成面向抽象编程,而真正关键的是抽象什么的问题。难道是抽象一个基类,然后让Wife
,Husband
,Son
继承自该基类吗?他们都是家庭成员,看似好像可行,但它们并没有公共的实现,而且如果后续再加入猫,老鼠或者其它什么的呢?就会变得更加风马牛不相及。面对这种未知的变化,显然很难抽象出一个公共的基类,而针对“观察事件发生”这个行为抽象出接口或许更合适。
演进一-简易观察者模式
根据这个思路,下面看看改进后的实现,先定义一个公共的接口:
public interface IObserver
{
void Update();
}
这里定义了一个跟任何子类都无关的void Update()
方法,这也是没办法的办法,因为我们不可能直接对Wakeup()
或者DreamTalk()
方法进行抽象,只能通过这种方式规范一个公共的行为接口,意思是当被观察的事件发生时,更新具体实例的某些状态。而具体实现类就简单了:
public class Wife : IObserver
{
public void Update()
{
Wakeup();
}
public void Wakeup()
{
Console.WriteLine("便有妇人惊觉欠伸");
}
}
public class Husband: IObserver
{
public void DreamTalk()
{
Console.WriteLine("其夫呓语");
}
public void Update()
{
DreamTalk();
}
}
public class Son : IObserver
{
public void Update()
{
Wakeup();
}
public void Wakeup()
{
Console.WriteLine("既而儿醒,大啼");
}
}
这里Update()
仅仅相当于做了一次转发,当然,也可以加入自己的逻辑。改变较大的是Dog
类,不过也都是前面组合模式,享元模式等中用过的常用手法,如下所示:
public class Dog
{
private readonly IList<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void Bark()
{
Console.WriteLine("遥闻深巷中犬吠");
foreach (var observer in _observers)
{
observer.Update();
}
}
}
不难理解,由于Wife
,Husband
,Son
都实现了IObserver
接口,因此可以通过IList<IObserver>
集合进行存储,同时通过AddObserver(IObserver observer)
和RemoveObserver(IObserver observer)
对具体实例进行添加和删除管理。
再看看调用的代码:
static void Main(string[] args)
{
Dog dog = new Dog();
Wife wife = new Wife();
Husband husband = new Husband();
Son son = new Son();
dog.AddObserver(wife);
dog.AddObserver(husband);
dog.AddObserver(son);
dog.Bark();
Console.WriteLine("----------------------");
dog.RemoveObserver(son);
dog.Bark();
}
其实,这就是需求最简单的观察者模式了,其中Dog
是被观察者,也就是被观察的主题,而Wife
,Husband
,Son
都是观察者,下面看看它的类图:
从这个类图上,我们可能会发现一个问题,既然观察者实现了一个抽象的接口,那么被观察者理所应当也应该实现一个抽象的接口啊,毕竟面向接口编程嘛!是的,但是该实现接口还是继承抽象类呢?我们暂且搁置,先叠加一个需求看看。
演进二
翻翻课本可以看到,“遥闻深巷中犬吠,便有妇人惊觉欠伸,其夫呓语。既而儿醒,大啼。”,后面还有三个字“夫亦醒。”(后面还有很多,为防止过于复杂,我们就不考虑了),我们再来看看他们之间的关系:
结合上下文可以知道,丈夫是被儿子哭声吵醒的,而不是狗叫。依据这些,我们可以分析出以下三点:
- 被观察者有两个,一个是狗,一个是儿子;
- 丈夫观察了两件事,一个是狗叫,一个是儿子哭;
- 儿子既是观察者,又是被观察者。
感觉一下子复杂了好多,不过好在有了前面的铺垫,实现起来,好像也并不是特别困难,Wife
和Dog
没有任何变化,主要需要修改的是Husband
和Son
,代码如下:
public class Husband : IObserver
{
public void DreamTalk()
{
Console.WriteLine("其夫呓语");
}
public void Update()
{
DreamTalk();
}
public void Wakeup()
{
Console.WriteLine("夫亦醒");
}
}
public class Son : IObserver
{
private readonly IList<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void Update()
{
Wakeup();
}
public void Wakeup()
{
Console.WriteLine("既而儿醒,大啼");
foreach (var observer in _observers)
{
observer.Update();
}
}
}
可以看到,Husband
多了一个Wakeup()
方法,Son
同时实现了观察者和被观察者的逻辑。
当然,调用的地方也有了一些变化,毕竟Son
的地位不同了,代码如下:
static void Main(string[] args)
{
Dog dog = new Dog();
Wife wife = new Wife();
Husband husband = new Husband();
Son son = new Son();
dog.AddObserver(wife);
dog.AddObserver(husband);
dog.AddObserver(son);
son.AddObserver(husband);
dog.Bark();
}
看到这里,细心的人会发现这段代码存在着很多问题,至少有以下两点:
Dog
和Son
中存在着大量重复的代码;- 运行一下会发现
Husband
的功能没有实现,因为Husband
中没有标识事件的类型或来源,因此也就不知道是该说梦话还是该醒过来。
演进三-标准观察者模式
为了解决上述两个问题,我们需要再做一次改进,首先第一个代码重复的问题,很明显提取一个共同的基类就可以解决,而第二个问题必须通过传参来加以区分了,我们可以先定义一个携带事件参数的类,事件参数通常至少包含事件来源以及事件类型(当然也可以包含其它的属性),代码如下:
public class EventData
{
public object Source { get; set; }
public string EventType { get; set; }
}
改造的观察者接口和提取的被观察者基类如下:
public interface IObserver
{
void Update(EventData eventData);
}
public class Subject
{
private readonly IList<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void Publish(EventData eventData)
{
foreach (var observer in _observers)
{
observer.Update(eventData);
}
}
}
可以看到,观察者IObserver
中加入了事件参数,被观察者Subject
既没有使用接口,也没有使用抽象类,原则上,这样是不合适的,但是,这个类中实在是没有抽象方法,也不适合用抽象类,所有只能勉强使用普通类了。
其它代码如下:
public class Wife : IObserver
{
public void Update(EventData eventData)
{
if (eventData.EventType == "DogBark")
{
Wakeup();
}
}
public void Wakeup()
{
Console.WriteLine("便有妇人惊觉欠伸");
}
}
public class Husband : IObserver
{
public void DreamTalk()
{
Console.WriteLine("其夫呓语");
}
public void Update(EventData eventData)
{
if (eventData.EventType == "DogBark")
{
DreamTalk();
}
else if (eventData.EventType == "SonCry")
{
Wakeup();
}
}
public void Wakeup()
{
Console.WriteLine("夫亦醒");
}
}
public class Son : Subject, IObserver
{
public void Update(EventData eventData)
{
if (eventData.EventType == "DogBark")
{
Wakeup();
}
}
public void Wakeup()
{
Console.WriteLine("既而儿醒,大啼");
Publish(new EventData { Source = this, EventType = "SonCry" });
}
}
public class Dog : Subject
{
public void Bark()
{
Console.WriteLine("遥闻深巷中犬吠");
Publish(new EventData { Source = this, EventType = "DogBark" });
}
}
可以看到,被观察者通过Publish(EventData eventData)
方法将事件发出,而观察者通过参数中的事件类型来决定接下来该执行什么动作,下面是它的类图:
这其实就是GOF定义的观察者模式了。
定义
多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
UML类图
将上述实例的类图简化一下,就可以得到如下观察者模式的类图了:
- Subject:抽象主题角色,它是一个抽象类(而实际上我用的是普通类),提供了一个用于保存观察者对象的集合和增加、删除以及通知所有观察者的方法。
- ConcreteSubject:具体主题角色。
- IObserver:抽象观察者角色,它是一个接口,提供了一个更新自己的方法,当接到具体主题的更改通知时被调用。
- Concrete Observer:具体观察者角色,实现抽象观察者中定义的接口,以便在得到主题的更改通知时更新自身的状态。
优缺点
优点
- 降低了主题与观察者之间的耦合关系;
- 主题与观察者之间建立了一套触发机制。
缺点
- 主题与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用;
- 当观察者对象很多时,事件通知会花费很多时间,影响程序的效率。
当然,这里的缺点指的是观察者模式的缺点,上述实例的缺点其实会更多,我们后续再想办法解决。
通知模式
其实观察者模式中,事件的通知无外乎两种模式-推模式和拉模式,这里简单的解释一下。我们上述的实现使用的都是推模式,也就是由主题主动将事件消息推送给观察者,好处就是实时高效,这也是较为推荐的一种方式。
但是并非所有场景都适合使用推模式,例如,某主题有非常多的观察者,但是每个观察者都只关注主题的某个或某些状态,这时使用推模式就不太合适了,因为推模式会将主题的所有状态不加区分的推送给所有观察者,对观察者而言,得到的消息就过于臃肿驳杂了。这时就可以采用拉模式了,主题公开所有可以被观察的状态,由观察者主动拉取自己关注的部分。
而拉模式根据不同情况又可以有两种实现。一种方式是由观察者定时检查,并拉取数据,这种操作简单粗暴,但是,会给主题造成较大的性能负担,同时,也会因为检查频率的不同而带来不同程度的延时。而另一种方式还是由主题主动发出通知,不过通知不带任何参数,仅仅是告诉观察者主题有变化了,然后由观察者去拉取自己关注的部分,这正是拉模式中最常采用的一种手段。
总结
好了,GOF定义的观察者模式分析完了,但实际上,观察者模式还远远没有结束,限于篇幅,我们在下一篇中接着分析。不过在这之前,可以提前思考一下下面两个问题:
dog.AddObserver(...)
真的合适吗?实际生活中,狗真的有这种能力吗?- 我们知道
C#
中不支持多继承,如果Dog
本身继承自Animal
的基类,如果同时作为被观察者,除了用上述演进一的实现,还能如何实现?因为这种场景太常见了。
想清楚这两个问题,观察者模式才可能真正的展现出它的威力。