总目录
前言
在现实生活中,处处可见观察者模式,例如,微信中的订阅号,只要对订阅号进行关注的客户端,如果订阅号有什么更新,就会直接推送给订阅了的用户。这就是观察者模式的一种应用。
1 基础介绍
- 观察者模式定义了对象之间的一种一对多的依赖关系,使得当一个对象状态发生改变时,它的所有依赖者都能够得到相应的通知并作出相应的反应。观察者模式也被称为发布-订阅模式。
- 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己的行为。
- 观察者模式可以实现对象之间的松耦合,从而使得对象更容易扩展和维护。同时,它也可以帮助我们实现一些实时通信的需求,如事件驱动的程序等。
- 观察者模式中首先会存在两个对象,一个是观察者对象,另一个就是主题对象,然而,根据面向接口编程的原则,则自然就有抽象主题角色和抽象观察者角色,因此其中的角色有:
- 抽象主题角色(Subject):抽象主题把所有观察者对象的引用保存在一个列表中,并提供增加和删除 观察者对象的操作,抽象主题角色又叫做抽象被观察者角色,一般由抽象类或接口实现。
- 抽象观察者角色(Observer):为所有具体观察者定义一个接口,在得到主题通知时更新自己,一般由抽象类或接口实现。
- 具体主题角色(ConcreteSubject):实现抽象主题接口,具体主题角色又叫做具体被观察者角色。
- 具体观察者角色(ConcreteObserver):实现抽象观察者角色所要求的接口,以便使自身状态与主题的状态相协调。
2 使用场景
-
1.在多个对象之间需要有一种一对多的依赖关系,并且不希望关键对象和被依赖对象之间存在强耦合关系。
-
2.需要实现实时通信,如事件驱动的程序等。
-
3.需要通知多个对象,但又不知道这些对象的确切数量和类型时。
-
4.需要将各个对象解耦开来,使得它们的改动不会影响到其他对象。
3 实现方式
1 案例1
在C#中,使用观察者模式的关键是要定义一个接口,包含一个Update()方法。这个方法表示观察者需要在被通知时执行的操作。
接口代码示例:
//观察者接口
public interface IObserver
{
void Update();
}
然后,在被观察者对象中定义一个List类型的观察者列表,并提供添加删除观察者的方法。被观察者对象的代码示例:
//被观察者抽象类
public abstract class Subject
{
private List<IObserver> _observers = new List<IObserver>();
//添加观察者
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
//移除观察者
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
//通知观察者
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update();
}
}
}
//被观察者实现类
public class ConcreteSubject:Subject
{
}
具体的观察者对象需要实现IObserver接口中的Update()方法,以便在被通知时能够执行相应的操作。观察者对象的代码示例:
//具体观察者类1
public class ConcreteObserverA : IObserver
{
public void Update()
{
Console.WriteLine("ConcreteObserverA received the message.");
}
}
//具体观察者类2
public class ConcreteObserverB : IObserver
{
public void Update()
{
Console.WriteLine("ConcreteObserverB received the message.");
}
}
最后,我们可以在客户端中创建具体的被观察者对象和观察者对象,并将观察者对象添加到被观察者对象的观察者列表中,从而实现观察者模式的功能。
class Client
{
static void Main(string[] args)
{
//创建被观察者对象
Subject subject = new ConcreteSubject();
//创建两个具体观察者对象并添加到被观察者对象的观察者列表中
IObserver observerA = new ConcreteObserverA();
IObserver observerB = new ConcreteObserverB();
subject.Attach(observerA);
subject.Attach(observerB);
//通知观察者对象
subject.Notify();
}
}
2 案例2
观察者模式在显示生活中也有类似的例子,比如:我们订阅银行短信业务,当我们账户发生改变,我们就会收到相应的短信。类似的还有微信订阅号,今天我们就以银行给我发送短信当我们账户余额发生变化的时候为例来讲讲观察者模式的实现,很简单,现实生活正例子也很多,理解起来也很容易。我们看代码吧,实现代码如下:
//银行短信系统抽象接口,是被观察者--该类型相当于抽象主体角色Subject
public abstract class BankMessageSystem
{
protected IList<Depositor> observers;
//构造函数初始化观察者列表实例
protected BankMessageSystem()
{
observers = new List<Depositor>();
}
//增加预约储户
public abstract void Add(Depositor depositor);
//删除预约储户
public abstract void Delete(Depositor depositor);
//通知储户
public void Notify()
{
foreach (Depositor depositor in observers)
{
if (depositor.AccountIsChanged)
{
depositor.Update(depositor.Balance, depositor.OperationDateTime);
//账户发生了变化,并且通知了,储户的账户就认为没有变化
depositor.AccountIsChanged = false;
}
}
}
}
//北京银行短信系统,是被观察者--该类型相当于具体主体角色ConcreteSubject
public sealed class BeiJingBankMessageSystem : BankMessageSystem
{
//增加预约储户
public override void Add(Depositor depositor)
{
//应该先判断该用户是否存在,存在不操作,不存在则增加到储户列表中,这里简化了
observers.Add(depositor);
}
//删除预约储户
public override void Delete(Depositor depositor)
{
//应该先判断该用户是否存在,存在则删除,不存在无操作,这里简化了
observers.Remove(depositor);
}
}
//储户的抽象接口--相当于抽象观察者角色(Observer)
public abstract class Depositor
{
//状态数据
private string _name;
private int _balance;
private int _total;
private bool _isChanged;
//初始化状态数据
protected Depositor(string name, int total)
{
this._name = name;
this._balance = total;//存款总额等于余额
this._isChanged = false;//账户未发生变化
}
//储户的名称,假设可以唯一区别的
public string Name
{
get { return _name; }
private set { this._name = value; }
}
public int Balance
{
get { return this._balance; }
}
//取钱
public void GetMoney(int num)
{
if (num <= this._balance && num > 0)
{
this._balance = this._balance - num;
this._isChanged = true;
OperationDateTime = DateTime.Now;
}
}
//账户操作时间
public DateTime OperationDateTime { get; set; }
//账户是否发生变化
public bool AccountIsChanged
{
get { return this._isChanged; }
set { this._isChanged = value; }
}
//更新储户状态
public abstract void Update(int currentBalance, DateTime dateTime);
}
//北京的具体储户--相当于具体观察者角色ConcreteObserver
public sealed class BeiJingDepositor : Depositor
{
public BeiJingDepositor(string name, int total) : base(name, total) { }
public override void Update(int currentBalance, DateTime dateTime)
{
Console.WriteLine(Name + ":账户发生了变化,变化时间是" + dateTime.ToString() + ",当前余额是" + currentBalance.ToString());
}
}
// 客户端(Client)
class Program
{
static void Main(string[] args)
{
//我们有了三位储户,都是武林高手,也比较有钱
Depositor huangFeiHong = new BeiJingDepositor("黄飞鸿", 3000);
Depositor fangShiYu = new BeiJingDepositor("方世玉", 1300);
Depositor hongXiGuan = new BeiJingDepositor("洪熙官", 2500);
BankMessageSystem beijingBank = new BeiJingBankMessageSystem();
//这三位开始订阅银行短信业务
beijingBank.Add(huangFeiHong);
beijingBank.Add(fangShiYu);
beijingBank.Add(hongXiGuan);
//黄飞鸿取100块钱
huangFeiHong.GetMoney(100);
beijingBank.Notify();
//黄飞鸿和方世玉都取了钱
huangFeiHong.GetMoney(200);
fangShiYu.GetMoney(200);
beijingBank.Notify();
//他们三个都取了钱
huangFeiHong.GetMoney(320);
fangShiYu.GetMoney(4330);
hongXiGuan.GetMoney(332);
beijingBank.Notify();
Console.Read();
}
}
3 案例3 通过委托事件来实现
当我们订阅了书社的信息,当书社有新书发布的时候,就通知我们
//事件发布者
public class Publisher
{
public string Name { get; set; }
//定义事件
public event EventHandler<Book> PublishBookEvent;
//定义调用事件的方法
//当事件发布的时候,通知订阅者
public void OnPublish(Book book)
{
//如果实际处理的业务中,不需要入参,object sender 和EventArg e 都可以传入null
PublishBookEvent?.Invoke(this, book);
}
}
//事件订阅者1
public class Subscriber1
{
public string Name { get; set; }
//定义 订阅方法,当触发发布事件的时候就调用该方法
public void SubscribeBookInfo(object sender, Book book)
{
if (sender is Publisher publisher)
{
Console.WriteLine($"发布者:{publisher.Name},订阅者{Name}:订阅了{book.Name}");
}
}
}
//事件订阅者2
public class Subscriber2
{
public string Name { get; set; }
public void SubscribeBookInfo(object sender, Book book)
{
if (sender is Publisher publisher)
{
Console.WriteLine($"发布者:{publisher.Name},订阅者{Name}:订阅了{book.Name}");
}
}
}
//EventArgs是EventHandler中传入参数的基类,因此需要传递参数的时候,参数都需要继承自EventArgs
public class Book : EventArgs
{
public string Name { get; set; }
}
订阅事件:
static void Main(string[] args)
{
//发布者
Publisher publisher = new Publisher() { Name = "李发布" };
//订阅者1 订阅事件
publisher.PublishBookEvent += new Subscriber1() { Name = "张订阅" }.SubscribeBookInfo!;
//订阅者2 订阅事件
publisher.PublishBookEvent += new Subscriber2() { Name = "王订阅" }.SubscribeBookInfo!;
//发布者发布事件
publisher.OnPublish(new Book() { Name = "平凡的世界" });
Console.ReadLine();
}
4 优缺点分析
-
优点:
- 观察者模式实现了表示层和数据逻辑层的分离,并定义了稳定的更新消息传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层,即观察者。
- 观察者模式在被观察者和观察者之间建立了一个抽象的耦合,被观察者并不知道任何一个具体的观察者,只是保存着抽象观察者的列表,每个具体观察者都符合一个抽象观察者的接口。
- 观察者模式支持广播通信。被观察者会向所有的注册过的观察者发出通知。
-
缺点:
- 如果一个被观察者有很多直接和间接的观察者时,将所有的观察者都通知到会花费很多时间。
- 虽然观察者模式可以随时使观察者知道所观察的对象发送了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎样发生变化的。
- 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃,在使用观察者模式应特别注意这点。
结语
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
C#设计模式之十六观察者模式(Observer Pattern)【行为型】
C#设计模式(17)——观察者模式(Observer Pattern)
C#设计模式15——观察者模式的写法