观察者模式(Observer)
先来看一个范例:
一般来说,报社的业务就是出版报纸,而作为用户呢,您可以在这家报社订阅报纸,
这样的话,只要报社有出了新报纸的时候,便会给订阅其报纸的用户发送,
也就是,只要你是报社的用户,那么当报社有新报纸的时候,报社就会给你送一份报纸过来,
同时,你也可以取消你在这家报社订阅的报纸,这样的话,你就不会收到由这家报社送过来的报纸了。
在来看下面的截图:
其实呢,上面描述的问题就是一个典型的观察者模式,
观察者模式也称为发布-订阅模式(Publish/Subscribe),
下面先给出观察者模式的定义吧,
观察者模式定义了对象之间的一对多依赖,让多个观察者同时监听某一个主题对象,
当这个主题对象发生改变时,便会通知所有的观察者更新。
就拿上面的例子来说的话,报社是依赖于用户群的,
也就是在对象报社和用户之间是一对多的关系,同时,当对象报社出了新报纸的时候,
便会通知其用户群接收报纸。
将上面的例子进行重命名,把“发布新报纸”改为主题(Subject),
而用户则改为观察者(Observer),这样就是一个完整的观察者模式了。
下面给出的上面的报纸发布订阅这个例子的类图:
这里需要解释的是为何会有多个的具体主题,其实这很好理解,
因为有多家报社,并且这些报社具有与众不同的特点,
然后就是观察者为何为有多个类呢,可以直接一个类完事的啊,其实这也好理解的,
订阅报纸的用户也就是观察者有可能是用户,也有可能是公司,
如果不考虑那么多的话,其实完全可以将上面的类图简化为下面所示:
下面就来看上面的例子如何转换成为代码了:
先来看主题抽象类 Subject 的代码
using System.Collections.Generic;
namespace Observer
{
public abstract class Subject
{
//用来存放所有的观察者对象
private IList<Observer> observers = new List<Observer>();
//注册一个观察者
public void RegisterObserver(Observer observer)
{
observers.Add(observer);
}
//移除一个观察者
public void RemoveObserver(Observer observer)
{
observers.Remove(observer);
}
//通知所有的观察者
public void NotifyObserver()
{
foreach (Observer observer in observers)
{
//调用观察者的 Update 来让他们自动更新
observer.Update();
}
}
}
}
再来看具体主题类 ConcreteSubject 的代码
namespace Observer
{
public class ConcreteSubject:Subject
{
//仅仅是添加了一个新属性而已
private string objectState;
public string ObjectState
{
get { return objectState; }
set { objectState = value; }
}
}
}
再来看观察者接口 Observer 的代码
namespace Observer
{
public interface Observer
{
/// <summary>
/// 在接口中定义了当观察者得到主题给的通知后
/// 自动更新自己的方法
/// </summary>
void Update();
}
}
还有一个就是具体的观察者 ConcreteObserver
using System;
namespace Observer
{
public class ConcreteObserver:Observer
{
//订阅报纸的用户名
private string userName;
//保存一个具体的主题对象,来获取主题提供的信息
private ConcreteSubject subject;
public ConcreteObserver(string userName,
ConcreteSubject subject)
{
this.userName = userName;
this.subject = subject;
}
//更新
public void Update()
{
Console.WriteLine("{0} ,您好,关于 {1} 的新报纸到了",
this.userName, subject.ObjectState);
}
}
}
最后再来看一下客户端的 Main 函数
using System;
namespace ObserverTest
{
class Program
{
static void Main(string[] args)
{
//实例化一个主题对象
Observer.ConcreteSubject subject =
new Observer.ConcreteSubject();
//指定主题的状态
subject.ObjectState = "伊朗核问题";
//定义多个观察者,此时的每一个观察者都还没有被注册
Observer.Observer observerOne =
new Observer.ConcreteObserver("用户一", subject);
Observer.Observer observerTwo =
new Observer.ConcreteObserver("用户二", subject);
Observer.Observer observerThree =
new Observer.ConcreteObserver("用户三", subject);
Observer.Observer observerFour =
new Observer.ConcreteObserver("用户四", subject);
Observer.Observer observerFive =
new Observer.ConcreteObserver("用户五", subject);
//注册三个观察者
subject.RegisterObserver(observerOne);
subject.RegisterObserver(observerTwo);
subject.RegisterObserver(observerThree);
//通知观察者新报纸到了
subject.NotifyObserver();
Console.Read();
}
}
}
最后的效果如下截图
在 Main 中我定义了 5 个观察者,但是却只注册了 3 个观察者给主题,
所以只有这三个注册了的观察者可以得到主题的通知并且自行更新自己。
上面的呢,就是关于观察者模式的一个基本介绍了,但是在这里还有几个地方要注意的:
首先,观察者模式呢,分为了推模式和拉模式,
何为推模式呢:推模式就是当有新的消息时,把消息以参数的形式传递给每个观察者。
而拉模式呢:是当有新消息时,并不把消息的信息以参数的形式传递给每个观察者,
而只是仅仅通知观察者有消息来到,而至于要不要提取出消息,那是观察者自己的事情了。
也就是说,消息的提取必须由观察者自行完成,而不是由主题对象统一广播给所有的观察者。
其实上面的例子中使用的就是拉模式,您可以看到在 Subject 这个抽象类中的 NotifyObserver 方法中,
public void NotifyObserver()
{
foreach (Observer observer in observers)
{
//调用观察者的 Update 来让他们自动更新
observer.Update();
}
}
上面的 Update()方法中并没有传递进去参数,
而是采用在 ConcreteObserver 类中保存了一个 ConcreteSubject 对象的引用,
//保存一个具体的主题对象,来获取主题提供的信息
private ConcreteSubject subject;
通过这个引用来获取更新的消息。
//更新
public void Update()
{
Console.WriteLine("{0} ,您好,关于 {1} 的新报纸到了",
this.userName, subject.ObjectState);
}
从上面的做法,不难得出拉模式的一些优点和缺点:
优点在于拉模式可以按需取得消息数据,
因为您可以在 Update()按需要来决定是否要得到(在这里就是显示)数据。
当然,缺点就是在 ConcreteObserver 中保存了 ConcreteSubject 对象的引用,
这样使得主题对象和观察者之间的耦合加强了。
下面将上面的 Demo 的一些代码进行修改从而实现推模式:
首先看 Subject 类吧
using System.Collections.Generic;
namespace Observer
{
public abstract class Subject
{
//用来存放所有的观察者对象
//依赖于抽象,而不是具体
protected IList<Observer> observers = new List<Observer>();
//注册一个观察者
public void RegisterObserver(Observer observer)
{
observers.Add(observer);
}
//移除一个观察者
public void RemoveObserver(Observer observer)
{
observers.Remove(observer);
}
//通知所有的观察者
public abstract void NotifyObserver();
}
}
然后就是看一下 ConcreteSubject 类了
namespace Observer
{
public class ConcreteSubject:Subject
{
//仅仅是添加了一个新属性而已
private string objectState;
public string ObjectState
{
get { return objectState; }
set { objectState = value; }
}
//通知所有的观察者
public override void NotifyObserver()
{
foreach (Observer observer in observers)
{
//调用观察者的 Update 来让他们自动更新
observer.Update(this);
}
}
}
}
然后就是 Observer 接口
namespace Observer
{
public interface Observer
{
/// <summary>
/// 在接口中定义了当观察者得到主题给的通知后
/// 自动更新自己的方法
/// </summary>
void Update(ConcreteSubject subject);
}
}
还有一个类就是 ConcreteObserver
using System;
namespace Observer
{
public class ConcreteObserver:Observer
{
//订阅报纸的用户名
private string userName;
public ConcreteObserver(string userName)
{
this.userName = userName;
}
//更新
public void Update(ConcreteSubject sub)
{
Console.WriteLine("{0} ,您好,关于 {1} 的新报纸到了",
this.userName, sub.ObjectState);
}
}
}
Main 函数当中的代码
using System;
namespace ObserverTest
{
class Program
{
static void Main(string[] args)
{
//实例化一个主题对象
Observer.ConcreteSubject subject =
new Observer.ConcreteSubject();
//指定主题的状态
subject.ObjectState = "伊朗核问题";
//定义多个观察者,此时的每一个观察者都还没有被注册
Observer.Observer observerOne =
new Observer.ConcreteObserver("用户一");
Observer.Observer observerTwo =
new Observer.ConcreteObserver("用户二");
Observer.Observer observerThree =
new Observer.ConcreteObserver("用户三");
Observer.Observer observerFour =
new Observer.ConcreteObserver("用户四");
Observer.Observer observerFive =
new Observer.ConcreteObserver("用户五");
//注册三个观察者
subject.RegisterObserver(observerOne);
subject.RegisterObserver(observerTwo);
subject.RegisterObserver(observerThree);
//通知观察者新报纸到了
subject.NotifyObserver();
Console.Read();
}
}
}
修改后的效果和前面是一模一样的
在这个修改后的例子中又可以看出推模式的优点和它的缺点,
优点是所有的观察者直接得到消息,因为在 Update()方法中直接将整个信息作为了参数传递。
同时,您不必再在 ConcreteObserver 中保存和维护一个 ConcreteSubject 对象了,
这样就减少了两者之间的耦合,
当然,其缺点也是很明显的,就是观察者不能按需所取,
即每次消息都是广播通知,每个观察者都将得到所有的信息,
从拉模式和推模式来看的话,两者是互补的,拉模式解决了推模式带来的缺点,
而推模式又解决了拉模式带来的缺点,
那么有没有一种办法可以同时继承拉模式和推模式的优点呢?
答案是有的~~~~~
上面所介绍的呢是单单的观察者设计模式,
而下面将要介绍的是就是如何实现继承拉模式和推模式的优点,
且不带入新的缺点这个问题
观察者模式在 . Net 里头的更好的解决办法---委托和事件
下面就通过将上面的那个 Demo 来改写为采用委托和事件完成
(对委托和事件不太了解的可以参考笔者的前面的一篇博文)
先来看 Subject 这个抽象类,您将发现在这个类中,
已经没有了添加观察者,删除观察者,以及保存所有的观察者这样的代码了,
只剩下一个通知所有观察者的方法了
using System.Collections.Generic;
using System;
namespace Observer
{
public abstract class Subject
{
//通知所有的观察者
public abstract void NotifyObserver();
}
}
再来看 ConcreteSubject 这个类
namespace Observer
{
public class ConcreteSubject:Subject
{
//定义一个委托类型,其有两个参数
//其中参数 SubjectEventArgs 是一种自定义的事件参数类型
public delegate void SubjectEventHandler(
object sender, SubjectEventArgs e);
//定义一个事件 Update ,且事件的委托类型为 EventHandler
public event SubjectEventHandler Update;
//仅仅是添加了一个新属性而已
private string objectState;
public string ObjectState
{
get { return objectState; }
set { objectState = value; }
}
public override void NotifyObserver()
{
if (Update != null)
{
//调用所有的更新
Update(this, new SubjectEventArgs(this));
}
}
}
}
下面再来看参数类型 SubjectEventArgs 类的代码
using System;
namespace Observer
{
public class SubjectEventArgs : EventArgs
{
//定义一个 ConcreteSubject 的属性
//也就是说要将一个 ConcreteSubject 类型的变量
//作为事件的参数传递
private ConcreteSubject sub;
public ConcreteSubject Sub
{
get { return sub; }
}
public SubjectEventArgs(ConcreteSubject sub)
{
this.sub = sub;
}
}
}
再来看接口 Observer
namespace Observer
{
public interface Observer
{
/// <summary>
/// 在接口中定义了当观察者得到主题给的通知后
/// 自动更新自己的方法
/// </summary>
void Update(object sender, SubjectEventArgs e);
}
}
还有一个类就是 ConcreteObserver
using System;
namespace Observer
{
public class ConcreteObserver:Observer
{
//订阅报纸的用户名
private string userName;
public ConcreteObserver(string userName)
{
this.userName = userName;
}
//更新
public void Update(object sender, SubjectEventArgs e)
{
Console.WriteLine("{0} ,您好,关于 {1} 的新报纸到了",
this.userName, e.Sub.ObjectState);
}
}
}
再来看一下 Main 函数就 OK 了
using System;
namespace ObserverTest
{
class Program
{
static void Main(string[] args)
{
//实例化一个主题对象
Observer.ConcreteSubject subject =
new Observer.ConcreteSubject();
//指定主题的状态
subject.ObjectState = "伊朗核问题";
//定义多个观察者,此时的每一个观察者都还没有被注册
Observer.Observer observerOne =
new Observer.ConcreteObserver("用户一");
Observer.Observer observerTwo =
new Observer.ConcreteObserver("用户二");
Observer.Observer observerThree =
new Observer.ConcreteObserver("用户三");
Observer.Observer observerFour =
new Observer.ConcreteObserver("用户四");
Observer.Observer observerFive =
new Observer.ConcreteObserver("用户五");
//绑定事件(注册事件)
subject.Update += observerOne.Update;
subject.Update += observerTwo.Update;
subject.Update += observerThree.Update;
//通知观察者新报纸到了
subject.NotifyObserver();
Console.Read();
}
}
}
上面的这个 Demo 的效果呢如下截图
从上面这个事件和委托实现观察者模式的 Demo 可以总结出,其确实实现了推模式和拉模式的优点的集成。
首先,它是按需取数据的,因为是将整个的 ConcreteSubject 作为一个参数进行传递的,
而后就是在 ConcreteObserver 中也不需要维护一个 ConcreteSubject 的对象的引用,
这样也实现了低耦合的要求。
关于观察者模式就介绍到这里了~~~~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器