上一篇说到了观察者模式较为传统的用法,这篇筹备分享点风行的,不过在开始新内容之前,咱们无妨先思考一下两种场景,一个是报社订阅报纸,另一个是在黑板上发布告,都是典型观察者模式利用场景,二者有何不同?
- 报社订阅报纸,订阅者须要到报社注销交钱,而后报社才会每次有新报纸时告诉到订阅者。
- 而在黑板上发布告,公布的人不晓得谁会看到,看到的人也不晓得是谁收回的,而事实上,看到布告的人也可能只是偶尔的机会瞟了一眼黑板而已。
能够看到,二者有显著的区别。前者,观察者必须要注册到被观察者上能力接管告诉;而后者,观察者和被观察者之间是互相齐全生疏的。回顾一下咱们在上一篇中举的例子,不难发现它其实相似第二种场景,狗叫并不知道谁会听见,而听的人也不是为了听狗叫,他仅仅是在关注外界的动静,恰好听到了狗叫而已。但咱们采纳的是相似第一种场景的解决形式,显然并不适合。因而,也就自然而然的留下了两个问题:
dog.AddObserver(...)
真的适合吗?理论生存中,狗真的有这种能力吗?- 咱们晓得
C#
中不反对多继承,如果Dog
自身继承自Animal
的基类,如果同时作为被观察者,除了用上述演进一的实现,还能如何实现?
针对这两个问题,该怎么解决了?无妨再回顾一下之前学过的设计准则,看看哪里能够寻找突破口。
一番考虑不难发现,主题类违反了 合成复用准则 ,也就是咱们常说的,HAS A
比IS A
更好。既然晓得 HAS A
更好,咱们为什么非得通过继承来实现性能的复用呢?更何况咱们继承的还是个一般类。
演进四 - 事件总线
基于这种思路,咱们能够试着把继承改成组合,不过在这之前,咱们无妨一步到位,罗唆再为 Subject
类定义一个形象的接口,省得看着不难受,毕竟面向形象编程嘛:
public interface ISubject
{void AddObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void Publish(EventData eventData);
}
public class Subject: ISubject
{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);
}
}
}
逻辑并没有任何改变,仅仅是实现了一个接口而已,这一步不做其实也没有关系。接下来该做什么应该也很分明了,没错,就是组合到被观察者中去,也就是 Dog
和Son
,上面是具体的实现:
public class Dog
{
private readonly ISubject _subject;
public Dog(ISubject subject)
{this._subject = subject;}
public void Bark()
{Console.WriteLine("遥闻深巷中犬吠");
_subject.Publish(new EventData { Source = this, EventType = "DogBark"});
}
}
public class Son : IObserver
{
private readonly ISubject _subject;
public Son(ISubject subject)
{this._subject = subject;}
public void Update(EventData eventData)
{if (eventData.EventType == "DogBark")
{Wakeup();
}
}
public void Wakeup()
{Console.WriteLine("既而儿醒,大啼");
_subject.Publish(new EventData { Source = this, EventType = "SonCry"});
}
}
批改的仅仅是被观察者,观察者不须要做任何扭转。看到下面的调用,不晓得大家有没有一种相熟的感觉呢?没错,这里的应用形式像极了微服务中罕用的事件总线 EventBus
,事实上,事件总线就是这么实现的,基本原理仅仅是观察者模式 继承 转组合 而已。
再看看调用的中央:
static void Main(string[] args)
{ISubject subject = new Subject();
Dog dog = new Dog(subject);
Wife wife = new Wife();
Husband husband = new Husband();
Son son = new Son(subject);
subject.AddObserver(wife);
subject.AddObserver(husband);
subject.AddObserver(son);
dog.Bark();}
将 Dog
与Subject
之间的关系改为 HAS A
之后,理论的事件收回者和事件接收者之间多了一层,使得二者之间齐全解耦了。这时,Dog
能够继承本人的 Animal
基类了,并且也不必再做相似在 Dog
类中治理 Wife
、Husband
、Son
这么奇怪的事了,对观察者的治理交给总线来实现。
再来看看这时的类图长什么样子:
如果感觉简单,能够不看 Dog
和Sun
这两个节点,只看实线框中的局部,有没有发现就是后面简易版的观察者模式呢?被观察者还是 Subject
,只不过和Dog
、Sun
曾经没什么关系了,这是多一层必然会导致的后果。到这里,其实曾经完满实现需求了,Subject
是原来的被观察者,但当初相当于事件总线,在程序启动的时候,将观察者全副注册到总线上就能够接管到总线上的事件音讯了。
演进五 -MQ
你认为这样就完了吗?其实并没有。再回到软件开发畛域,咱们晓得,事件的触发能够产生在零碎外部,也能够产生在零碎之间。而后面无论哪种形式的实现,其实解决的都是外部问题,那如果须要跨零碎该怎么办呢?间接调用的话,会像上篇当中的第一个实现一样,呈现强耦合,只不过这时调用的不再是一般的办法,而是跨网络的 API,而强耦合的也不再是类与类之间,而是零碎与零碎之间。并且随着事件数量的增多,也会使得调用链变得凌乱不堪,难以治理。
为了解决这个问题,就须要在所有零碎之外,退出一个两头代理的角色,所有发布者将事件音讯按不同主题发送给代理,而后代理再依据观察者关注主题的不同,将音讯分发给相应的观察者,当然,前提是发布者和观察者都提前在代理这里实现注册注销。
咱们先模仿实现一个代理,当然,我这里只是通过单例模式实现一个简略的示例,真实情况会比这个简单的多:
public class Broker
{
private static readonly Lazy<Broker> _instance
= new Lazy<Broker>(() => new Broker());
private readonly Queue<EventData> _eventDatas = new Queue<EventData>();
private readonly IList<IObserver> _observers = new List<IObserver>();
private readonly Thread _thread;
private Broker()
{_thread = new Thread(Notify);
_thread.Start();}
public static Broker Instance
{
get
{return _instance.Value;}
}
public void AddObserver(IObserver observer)
{_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{_observers.Remove(observer);
}
private void Notify(object? state)
{while (true)
{if (_eventDatas.Count > 0)
{var eventData = _eventDatas.Dequeue();
foreach (var observer in _observers)
{observer.Update(eventData);
}
}
Thread.Sleep(1000);
}
}
public void Enqueue(EventData eventData)
{_eventDatas.Enqueue(eventData);
}
}
这里通过单例模式定义了一个 Broker
代理类,理论状况下,这部分是由一个永不停机的 MQ 服务承当,次要包含四个局部组成:
- 一个
Queue<EventData>
类型的队列,用于寄存事件音讯; - 一组注册和登记观察者的办法;
- 一个接管来自事件发布者的事件音讯的办法;
- 最初就是事件音讯的告诉机制,这里用的是定时轮询的形式,理论利用中必定不会这么简略。
事实上,上述四个局部都应该针对不同的主题实现,也就是咱们经常会提到的 Topic,简直所有的 MQ 都会有 Topic 的概念,为了简略,咱们这里就不思考了。
再来看看 Subject
的实现:
public interface ISubject
{void Publish(EventData eventData);
}
public class Subject: ISubject
{public void Publish(EventData eventData)
{Broker.Instance.Enqueue(eventData);
}
}
因为对 IObserver
的治理交给了 Broker
代理,因而这里就不须要再关注具体的观察者是谁,也不须要治理观察者了,只须要负责公布事件就行了。须要留神的是,事件音讯公布给了 Broker
,后续的所有工作交给Broker
全权处理,观察者仍然不须要做任何代码上的批改。
调用的中央波及到的扭转次要体现在观察者的注册上,毕竟管理者不再是 Subject
,而是交由Broker
代理接管了:
static void Main(string[] args)
{ISubject subject = new Subject();
Dog dog = new Dog(subject);
Wife wife = new Wife();
Husband husband = new Husband();
Son son = new Son(subject);
Broker.Instance.AddObserver(wife);
Broker.Instance.AddObserver(husband);
Broker.Instance.AddObserver(son);
dog.Bark();}
乍一看,事件变得越来越简单了,这里为了解决跨零碎的问题,又套了一层,类图有点简单,为防止凌乱,我就不画了。不过好在思路的演进是清晰的,达到当初的后果,应该也不会感觉突兀,这个其实就是以后流行的 MQ 的根本实现思路了。
演进过程
通过后面一系列的革新,咱们解决了不同场景下的事件处理问题。接下来,咱们再次梳理一下观察者模式的整个演进过程,先看一张图:
这张图显示了观察者模式演进的不同阶段,主题与观察者之间的调用关系:
- 第一阶段升高了主题与观察者之间的耦合度,但并没有齐全解耦,这种状况次要利用在相似报纸订阅的场景;
- 第二阶段在主题与观察者之间加了一条总线,使得主题与观察者齐全解耦,这种状况次要使用在相似黑板发布公告的场景,但该实现难以应答跨零碎的事件处理;
- 第三阶段在总线与观察者之间又加了一个代理,使得存在于不同零碎之间的主题与观察者也可能解耦并且失常通信了。
能够看出,他们都有各自的利用场景,并不能简略的说谁更先进,谁能代替谁。能够预感,观察者模式将来可能还会持续演进,去应答更多新的更简单的场景。
.Net 中的利用
既然观察者模式这么好用,那.Net 框架中天然也会内置一些解决机制了。
- 在.Net 我的项目中,委托 (
delegate
) 和事件 (event
) 就是观察者模式的很好的一种实际,不过须要留神的是,委托和事件,严格意义上讲,曾经不能称之为设计模式了,因为它们针对的都是办法,跟面向对象设计无关,不过倒是能够称之为习用法。不过不论怎么样,它们要解决的问题跟观察者模式是统一的。 - .Net 中提供了一组泛型接口
IObserver<T>
和IObservable<T>
可用于实现事件告诉机制,顾名思义,前者相当于观察者,后者相当于主题。
这里就不列代码,免得喧宾夺主了,因为这不是本文的重点。而且前者太罕用了,应该没什么人不会。而后者呢,不晓得大家用的多不多,但其实我本人没怎么用,我更违心依据不同的场景来定义语义更明确的接口,如 ISender
用于发送,IProducer
用于生产,IListener
用于监听,IConsumer
用于生产等。
总结
事件无处不在,毫不夸大的说,整个世界的运行都是由事件驱动的。因而观察者模式也是无处不在的。咱们晓得,设计模式通过这么多年的倒退,曾经有了很大的变动,有的下沉变成了某些语言的习用法,例如前面会讲到的迭代器模式,有些回升更偏差于架构模式,例如后面讲过的外观模式。甚至有的被淘汰,例如备忘录模式。然而观察者模式却是惟一一个向上可用于架构设计,向下被实现为习用法,两头还能重构代码,几乎无处不在,无所不能。并且能够预感,将来也必然是经久不衰。
说的有点夸大了,不过也的确阐明观察者模式再怎么器重也不为过了!
源码链接