设计模式原则最终的落脚点:优化依赖关系,改善这种抵御变化的能力,使用面向对象改善这种情况,遵循依赖倒置原则。
模型–动机–意图–结构
在现实生活中,处处可见观察者模式,例如,微信中的订阅号,订阅博客和QQ微博中关注好友,这些都属于观察者模式的应用。在这一章将分享我对观察者模式的理解,废话不多说了,直接进入今天的主题。


从生活中的例子可以看出,只要对订阅号进行关注的客户端,如果订阅号有什么更新,就会直接推送给订阅了的用户。从中,我们就可以得出观察者模式的定义。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己的行为。

从上面观察者模式的定义和生活中的例子,很容易知道,观察者模式中
1、首先会存在两个对象,一个是观察者对象,另一个就是主题对象,
2、然而,根据面向接口编程的原则,则自然就有抽象主题角色和抽象观察者角色。
3、理清楚了观察者模式中涉及的角色后,接下来就要理清他们之间的关联了,要想主题对象状态发生改变时,能通知到所有观察者角色,则自然主题角色必须所有观察者的引用,这样才能在自己状态改变时,通知到所有观察者。

抽象后、优化依赖关系如上图
有了上面的分析,下面观察者的结构图也就很容易理解了。具体结构图如下所示:

可以看出,在观察者模式的结构图有以下角色:

下面以微信订阅号的例子来说明观察者模式的实现。现在要实现监控腾讯游戏订阅号的状态的变化。
这里一开始不采用观察者模式来实现,而通过一步步重构的方式,最终重构为观察者模式。因为一开始拿到需求,自然想到有两个类,一个是腾讯游戏订阅号类,另一个是订阅者类。订阅号类中必须引用一个订阅者对象,这样才能在订阅号状态改变时,调用这个订阅者对象的方法来通知到订阅者对象。有了这个分析,自然实现的代码如下所示:
// 腾讯游戏订阅号类
public class TenxunGame
{
// 订阅者对象
public Subscriber Subscriber {get;set;}
public String Symbol {get; set;}
public string Info {get ;set;}
public void Update()
{
if (Subscriber != null)
{
// 调用订阅者对象来通知订阅者
Subscriber.ReceiveAndPrintData(this);
}
}
}
// 订阅者类
public class Subscriber
{
public string Name { get; set; }
public Subscriber(string name)
{
this.Name = name;
}
public void ReceiveAndPrintData(TenxunGame txGame)
{
Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, tx
Game.Symbol, txGame.Info);
}
}
// 客户端测试
class Program
{
static void Main(string[] args)
{
// 实例化订阅者和订阅号对象
Subscriber LearningHardSub = new Subscriber("LearningHard");
TenxunGame txGame = new TenxunGame();
txGame.Subscriber = LearningHardSub;
txGame.Symbol = "TenXun Game";
txGame.Info = "Have a new game published ....";
txGame.Update();
Console.ReadLine();
}
}
上面代码确实实现了监控订阅号的任务。但这里的实现存在下面几个问题:
上面的设计违背了“开放——封闭”原则,显然,这不是我们想要的。

抽象后、优化依赖关系如上图*
对此我们要做进一步的抽象,既然这里变化的部分是新订阅者的出现,这样我们可以对订阅者抽象出一个接口,用它来取消TenxunGame类与具体的订阅者之间的依赖,做这样一步改进,确实可以解决TenxunGame类与具体订阅者之间的依赖,使其依赖与接口,从而形成弱引用关系,但还是不能解决出现一个订阅者不得不修改TenxunGame代码的问题。
对此,我们可以做这样的思考——
订阅号存在多个订阅者,我们可以采用一个列表来保存所有的订阅者对象,在订阅号内部再添加对该列表的操作,这样不就解决了出现新订阅者的问题了嘛。并且订阅号也属于变化的部分,所以,我们可以采用相同的方式对订阅号进行抽象,抽象出一个抽象的订阅号类。
这样也就可以完美解决上面代码存在的问题了,具体的实现代码为:
// 订阅号抽象类
public abstract class TenXun
{
// 保存订阅者列表
private List<IObserver> observers = new List<IObserver>();
public string Symbol { get; set; }
public string Info { get; set; }
public TenXun(string symbol, string info)
{
this.Symbol = symbol;
this.Info = info;
}
#region 新增对订阅号列表的维护操作
public void AddObserver(IObserver ob)
{
observers.Add(ob);
}
public void RemoveObserver(IObserver ob)
{
observers.Remove(ob);
}
#endregion
public void Update()
{
// 遍历订阅者列表进行通知
foreach (IObserver ob in observers)
{
if (ob != null)
{
ob.ReceiveAndPrint(this);
}
}
}
}
// 具体订阅号类
public class TenXunGame : TenXun
{
public TenXunGame(string symbol, string info)
: base(symbol, info)
{
}
}
// 订阅者接口
public interface IObserver
{
void ReceiveAndPrint(TenXun tenxun);
}
// 具体的订阅者类
public class Subscriber : IObserver
{
public string Name { get; set; }
public Subscriber(string name)
{
this.Name = name;
}
public void ReceiveAndPrint(TenXun tenxun)
{
Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, te
nxun.Symbol, tenxun.Info);
}
}
// 客户端测试
class Program
{
static void Main(string[] args)
{
TenXun tenXun = new TenXunGame("TenXun Game", "Have a new game published ....");
// 添加订阅者
tenXun.AddObserver(new Subscriber("Learning Hard"));
tenXun.AddObserver(new Subscriber("Tom"));
tenXun.Update();
Console.ReadLine();
}
}
上面代码是我们进行重构后的实现,重构后的代码实现类图如下所示

从上图可以发现,这样的实现就是观察者模式的实现。这样,在任何时候,只要调用了TenXun类的Update方法,它就会通知所有的观察者对象,同时,可以看到,观察者模式,取消了直接依赖,变为间接依赖,这样大大提供了系统的可维护性和可扩展性。这里并不是直接给出观察者模式的实现,而是通过一步步重构的方式来引出观察者模式的实现,相信通过这个方式,大家可以更深刻地理解观察者模式所解决的问题和带来的好处。
在.NET中,我们可以使用委托与事件来简化观察者模式的实现,上面的例子用事件和委托的实现如下代码所示:
namespace ObserverInNET
{
class Program
{
// 委托充当订阅者接口类
public delegate void NotifyEventHandler(object sender);
// 抽象订阅号类
public class TenXun
{
public NotifyEventHandler NotifyEvent;
public string Symbol { get; set; }
public string Info { get; set; }
public TenXun(string symbol, string info)
{
this.Symbol = symbol;
this.Info = info;
}
#region 新增对订阅号列表的维护操作
public void AddObserver(NotifyEventHandler ob)
{
NotifyEvent += ob;
}
public void RemoveObserver(NotifyEventHandler ob)
{
NotifyEvent ‐= ob;
}
#endregion
public void Update()
{
if (NotifyEvent != null)
{
NotifyEvent(this);
}
}
}
// 具体订阅号类
public class TenXunGame: TenXun
{
public TenXunGame(string symbol, string info)
: base(symbol, info)
{
}
}
// 具体订阅者类
public class Subscriber
{
public string Name { get; set; }
public Subscriber(string name)
{
this.Name = name;
}
public void ReceiveAndPrint(Object obj)
{
TenXun tenxun = obj as TenXun;
if (tenxun != null)
{
Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, tenxun.Symbol, tenxun.Info);
}
}
}
static void Main(string[] args)
{
TenXun tenXun = new TenXunGame("TenXun Game", "Have a new game published ....");
Subscriber lh = new Subscriber("Learning Hard");
Subscriber tom = new Subscriber("Tom");
// 添加订阅者
tenXun.AddObserver(new NotifyEventHandler(lh.ReceiveAndPrint));
tenXun.AddObserver(new NotifyEventHandler(tom.ReceiveAndPrint));
tenXun.Update();
Console.WriteLine("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐");
Console.WriteLine("移除Tom订阅者");
tenXun.RemoveObserver(new NotifyEventHandler(tom.ReceiveAndPrint));
tenXun.Update();
Console.ReadLine();
}
}
}
从上面代码可以看出,使用事件和委托实现的观察者模式中,减少了订阅者接口类的定义,此时,.NET中的委托正式充到订阅者接口类的角色。使用委托和事件,确实简化了观察者模式的实现,减少了一个IObserver接口的定义。
在下面的情况下可以考虑使用观察者模式:
到这里,观察者模式的分享就介绍了。观察者模式定义了一种一对多的依赖关系,让多个观察者对象可以同时监听某一个主题对象,这个主题对象在发生状态变化时,会通知所有观察者对象,使它们能够自动更新自己,解决的是“当一个对象的改变需要同时改变多个其他对象”的问题。大家可以以微信订阅号的例子来理解观察者模式。
再比如现在很多的消息中间件,RabbitMQ,kafka等,都有发布订阅功能,当一个消息发布后,所有订阅这个消息的队列都会收到这个消息,然后消费者负责去消费这些消息。
当然不能简单的将发布-订阅模式与观察者模式混为一谈,发布-订阅模式更像是观察者模式的扩展,不过概念上还是非常相似的。
发布-订阅模式中,订阅方可能并不知道发布方是谁,发布方也不需要同步的将状态通知到所有订阅方,他们之间的联系需要消息代理中间件来建立。所以发布-订阅的耦合更低,更利于扩展。
————————————————

上文中提到了观察者模式和发布——订阅模式,我们来总结一下两者差异:

在观察者模式中,观察者是知道Subject的,Subject记录了所有的观察者。
然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。
而发布-订阅模式大多数时候是异步的(使用消息队列)。
观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
尽管它们之间有区别,但有些人可能会说发布-订阅模式是观察者模式的变异,因为它们概念上是相似的。
在观察者模式中的Subject就像一个发布者(Publisher),观察者(Observer)就是订阅者(Subscriber)。
Subject通知观察者(Observer)就像发布者(Publisher)通知他的订阅者(Subscriber)。
所以很多书和文章使用发布-订阅概念来解释观察者设计模式。
但是这里还有另外一个流行的模式叫做发布-订阅设计模式。
它的概念和观察者模式非常类似。最大的区别是:
在发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者(Subscriber)。
意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。
那么如何过滤消息的呢?事实上这里有几个过程,最流行的方法是:基于主题以及基于内容。




那么任务订阅功能中有两类主体:
宗门任务大殿
上面宗门任务大殿与弟子间的关系其实就构成了一个观察者模式。
在观察者模式中,只有两种主体:
目标对象 (Object) 和 观察者 (Observer)。宗门任务大殿就是目标对象,弟子们就是观察者。
目标对象 Subject:
观察者 Observer 需要实现 update 方法,供目标对象调用。
update方法中可以执行自定义的业务逻辑 ———— 弟子们需要定义接收任务通知后的方法,例如去抢任务或任务不适合,继续等待下一个任务
我们把上面的文字形象化一下:

class Observer {
constructor(name) {
this.name = name;
}
update({taskType, taskInfo}) {
// 假设任务分为日常route和战斗war
if (taskType === "route") {
console.log(`${this.name}不需要日常任务`);
return;
}
this.goToTaskHome(taskInfo);
}
goToTaskHome(info) {
console.log(`${this.name}去任务大殿抢${info}任务`);
}
}
class Subject {
constructor() {
this.observerList = []
}
addObserver(observer) {
this.observerList.push(observer);
}
notify(task) {
console.log("发布五星任务");
this.observerList.forEach(observer => observer.update(task))
}
}
const subject = new Subject();
const stu1 = new Observer("弟子1");
const stu2 = new Observer("弟子2");
// stu1 stu2 购买五星任务通知权限
subject.addObserver(stu1);
subject.addObserver(stu2);
// 任务殿发布五星战斗任务
const warTask = {
taskType: 'war',
taskInfo: "猎杀时刻"
}
// 任务大殿通知购买权限弟子
subject.notify(warTask);
// 任务殿发布五星日常任务
const routeTask = {
taskType: 'route',
taskInfo: "种树浇水"
}
subject.notify(routeTask);
再给举个栗子: 比如你要应聘阿里巴巴的前端工程师,结果阿里巴巴 HR 告诉你没坑位了,留下你的电话,等有坑位联系你。于是,你美滋滋的留下了联系方式。殊不知,HR 已经留下了好多联系方式。好在 2022 年 2 月 30 号那天,阿里巴巴有了前端工程师的坑位,HR 挨着给留下的联系方式联系了一通。
案例中阿里巴巴就是目标对象 Subject ,联系方式列表就是用来维护观察者的 observerList ,根据前端职位的有无来调用 notify 方法。
那什么是发布订阅模式呐?
基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。
因此发布订阅模式与观察者模式相比,发布订阅模式中有三个角色,发布者 Publisher ,事件调度中心 Event Channel ,订阅者 Subscriber 。
上面的文字有些难以理解,我们继续以弟子领取任务为栗子,宗门感觉把任务订阅放在任务大殿中有些繁琐,于是决定在任务大殿和弟子中间添加中介。弟子在中介中订阅其需要的任务类型,当任务大殿发布任务后,中介会将发布任务给对应的订阅者。
宗门任务大殿: 任务发布者 —— Publisher
中介功能 —— Event Channel
维护任务类型,以及每种任务下的订阅情况
给订阅者提供订阅功能 —— == subscribe 功能==
当宗门发布任务后,中介会给所有的订阅者发布任务 ——== publish 功能==

以目前的热播剧开端为例,临近过年,摸鱼的心思越来越重,每天就迫不及待的等开端更新,想在开端更新的第一刻就开始看剧,那你会怎么做那?总不能时时刻刻刷新页面吧。平台提供了消息订阅功能,如果你选择订阅,平台更新开端后,会第一时间发消息通知你,订阅后,你就可以愉快的追剧了。
上面案例中,开端就是发布者 Publisher,追剧人就是订阅者 Subscribe,平台则承担了事件通道 Event Channel 功能。
class PubSub {
constructor() {
// 事件中心
// 存储格式: warTask: [], routeTask: []
// 每种事件(任务)下存放其订阅者的回调函数
this.events = {}
}
// 订阅方法
subscribe(type, cb) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].push(cb);
}
// 发布方法
publish(type, ...args) {
if (this.events[type]) {
this.events[type].forEach(cb => cb(...args))
}
}
// 取消订阅方法
unsubscribe(type, cb) {
if (this.events[type]) {
const cbIndex = this.events[type].findIndex(e=> e === cb)
if (cbIndex != -1) {
this.events[type].splice(cbIndex, 1);
}
}
if (this.events[type].length === 0) {
delete this.events[type];
}
}
unsubscribeAll(type) {
if (this.events[type]) {
delete this.events[type];
}
}
}
// 创建一个中介公司
let pubsub = new PubSub();
// 弟子一订阅战斗任务
pubsub.subscribe('warTask', function (taskInfo){
console.log("宗门殿发布战斗任务,任务信息:" + taskInfo);
})
// 弟子一订阅战斗任务
pubsub.subscribe('routeTask', function (taskInfo) {
console.log("宗门殿发布日常任务,任务信息:" + taskInfo);
});
// 弟子三订阅全类型任务
pubsub.subscribe('allTask', function (taskInfo) {
console.log("宗门殿发布五星任务,任务信息:" + taskInfo);
});
// 发布战斗任务
pubsub.publish('warTask', "猎杀时刻");
pubsub.publish('allTask', "猎杀时刻");
// 发布日常任务
pubsub.publish('routeTask', "种树浇水");
pubsub.publish('allTask', "种树浇水");