1. 观察者模式简介
- 软件系统:一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变,它们之间将产生联动;
- 观察者模式:
- 定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象;
- 发生改变的对象称为观察目标(主题对象),被通知的对象称为观察者;
- 一个观察目标可以对应多个观察者。
- 观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
- 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(观察目标),使用面向对象技术,将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
——《设计模式》GoF观察者模式别名:
发布-订阅(Publish/Subscribe)模式
模型-视图(Model/View)模式
源-监听器(Source/Listener)模式
从属者(Dependents)模式
2. 观察者模式的结构
- 抽象主题(Subject)角色:抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如哈希表)里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,主题角色又叫做抽象被观察者(Observable)角色,一般用一个抽象类或者一个接口实现。
- 抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。这个接口叫做更新接口。抽象观察者角色一般用一个抽象类或者一个接口实现。更新方法一般包括增删改等。
- 具体主题(ConcreteSubject)角色:将有关状态(即观察者需要了解的数据改变)存入具体现察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知(通知中携带改变的数据)。具体主题角色又叫做具体被观察者角色(Concrete Observable)。具体主题角色通常用一个具体子类实现。
- 具体观察者(ConcreteObserver)角色:具体现察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。如果需要,具体现察者角色可以保存一个指向具体主题对象的引用(允许观察者调用主题对象)。具体观察者角色通常用一个具体子类实现。
- 抽象主题角色与抽象观察者角色之间的合成关系,代表具体主题对象可以有任意多个对抽象观察者对象的引用。之所以使用抽象观察者而不是具体观察者,意味着主题对象不需要知道引用了哪些ConcreteObserver类型,而只知道抽象Observer类型。这就使得具体主题对象可以动态地维护一系列的对观察者对象的引用,并在需要的时候调用每一个观察者共有的Update()方法。这种做法即为前述的“针对抽象编程“—依赖倒置原则。
代码模式:
典型的抽象目标类代码:
using System.Collection
abstract class Subject
{
//定义一个观察者集合用于存储所有观察者对象
protected ArrayList observers = new ArrayList();
//声明抽象注册方法,用于向观察者集合中增加一个观察者
public abstract void Attach(Observer observer);
//声明抽象注销方法,用于在观察者集合中删除一个观察者
public abstract void Detach(Observer observer);
//声明抽象通知方法
public abstract void Notify();
}
典型的具体目标类代码:
class ConcreteSubject : Subject
{
public override void Attach(Observer observer)
{
observers.Add(observer);
}
public override void Detach(Observer observer)
{
observers.Remove(observer);
}
//实现通知方法
public override void Notify()
{
//遍历观察者集合,调用每一个观察者的响应方法
foreach(object obs in observers)
{
((Observer)obs).Update();
}
}
}
典型的抽象观察者代码:
interface Observer
{
void Update();
}
典型的具体观察者代码:
class ConcreteObserver : Observer
{
//实现响应方法
public void Update()
{
//具体更新代码
}
}
典型的客户端代码片段:
……
Subject subject = new ConcreteSubject();
Observer observer = new ConcreteObserver();
subject.Attach(observer); //通常由观察者自己注册
subject.Notify(); //通常由主题方自己发布
……
3. 观察者模式的示例
3.1 示例一
银行帐户改动通知系统:帐户的余额发生变化时,需要通过手机短信和E-Mail的方式通知到用户本人。
示例:BankAccount
示例: ObserverBankAccount
BankAccount示例代码:
namespace BankAccount{
class Program{
static void Main(string[] args){
BankAccount account = new BankAccount();
account.UpdateBalance(100);//余额加100
account.UpdateBalance(-50);//余额减50
}
}
}
namespace BankAccount{
// 银行帐户
class BankAccount{
// 帐户余额
private double balance = 0; //--观察者关心的状态-数据
//用到的通知操作对象--此处与这些具体类产生耦合
Email email = new Email();
Mobile mobile = new Mobile();
// 存入/取出--调整余额
// <param name="val"></param>
public void UpdateBalance(double val){
balance += val;
//--修改后必须通知观察者,在此调用观察者的方法(各不相同)进行处理,此处耦合度较高
//更不能将处理过程的代码在此处实现
email.SendEMail("user@163.com", balance);
mobile.SendMessage("1111111111", balance);
}
}
}
namespace BankAccount{
// 帐户余额发生变化时,发送Email通知用户
class Email{
/// 发送Email通知操作
/// <param name="address">用户邮件地址</param>
/// <param name="val">余额数值</param>
public void SendEMail(string address, double val){
Console.WriteLine("发送邮件到{0}, 余额为{1}.", address, val);
}
//其他操作相关的属性和方法
}
}
namespace BankAccount{
/// 帐户余额发生变化时,通过手机发送短信通知用户
class Mobile{
public void SendMessage(string phone_number, double val){
Console.WriteLine("发送短信至手机{0},余额为{1}.", phone_number, val);
}
//其他操作相关的属性和方法
}
}
ObserverBankAccount示例代码
namespace ObserverBankAccount{
class Program{
static void Main(string[] args){
//创建数据服务对象,因为后面要调用的UpdateBalance()方法在具体类中,此处定义不能使用抽象主题类
BankAccount account = new BankAccount();
//创建观察者对象,后面没有使用Email和Mobile的特殊方法,所以此处使用抽象观察者来定义
AccountUpdated email = new Email("myemail@163.com");
AccountUpdated mobile = new Mobile("12345678900");
//注册观察者
account.AddObserver(email);
account.AddObserver(mobile);
//改变数据-->自动通知所有的观察者
account.UpdateBalance(100);
//取消一个观察者
account.RemoveObserver(mobile);
Console.WriteLine("\n\n删除手机短信观察者后...");
//再次改变数据-->取消的观察者将不能收到数据更新
account.UpdateBalance(-50);
Console.Read();
}
}
}
namespace ObserverBankAccount{
// Observer--抽象观察者接口
public interface AccountUpdated{
// 观察者接收/处理通知的方法
// <param name="val">帐户余额</param>
void BalanceUpdated(double val);
}
}
namespace ObserverBankAccount{
/// 具体观察者--帐户余额发生变化时,发送Email通知用户
class Email:AccountUpdated{
// 接收邮件通知的E-Mail地址
private string EmailAddress;
public Email(string address){EmailAddress = address;}
// 发送Email通知操作
// <param name="val">余额数值</param>
public void BalanceUpdated(double val){
Console.WriteLine("发送邮件到{0}, 余额为{1}.", EmailAddress, val);
}
//其他操作相关的属性和方法
}
}
namespace ObserverBankAccount{
// 具体观察者--帐户余额发生变化时,通过手机发送短信通知用户
class Mobile:AccountUpdated{
/// 接收短信通知的电话号码
private string PhoneNumber;
public Mobile(string phone){PhoneNumber = phone;}
public void BalanceUpdated(double val){
Console.WriteLine("发送短信至手机{0},余额为{1}.", PhoneNumber, val);
}
//其他操作相关的属性和方法
}
}
namespace ObserverBankAccount{
// 抽象主题
abstract class Subject{
/// 观察者列表
List<AccountUpdated> observers = new List<AccountUpdated>();
/// 添加一个观察者
/// <param name="obs"></param>
public void AddObserver(AccountUpdated obs){
observers.Add(obs);
}
/// 删除一个已有的观察者
/// <param name="obs"></param>
public void RemoveObserver(AccountUpdated obs)
{
observers.Remove(obs);
}
/// 通知所有的观察者--数据已经更新-->具体的数据处理操作由各个观察者自己处理
/// <param name="val"></param>
protected virtual void Notify(double val)
{
foreach (AccountUpdated obs in observers)
obs.BalanceUpdated(val);
}
}
}
namespace ObserverBankAccount{
/// ConcreteSubject具体主题--银行帐户
class BankAccount:Subject{
/// 帐户余额--subjectState
private double balance=0;
/// 存入/取出--调整余额
/// <param name="val"></param>
public void UpdateBalance(double val){
balance += val;
Notify(balance); //调用父类的方法, 通知所有的观察者--〉余额改变了!
}
}
}
3.2 示例二
在某多人联机对战游戏中,多个玩家可以加入同一战队组成联盟,当战队中某一成员受到敌人攻击时,将给所有盟友发送通知,盟友收到通知后将作出响应。
现使用观察者模式设计并实现该过程,以实现战队成员之间的联动。
实现了主题对象与观察者,观察者之间的多向沟通
示例代码:
namespace ObserverSample{
/// 抽象指挥中心--抽象目标类
abstract class AllyControlCenter{
//战队名称
protected string allyName;
//定义一个集合用于存储战队成员--观察者列表
protected List<IObserver> players = new List<IObserver>();
public void SetAllyName(string allyName){this.allyName = allyName;}
public string GetAllyName(){return this.allyName;}
//注册方法--加入观察者--具体方法
public void Join(IObserver obs) {
Console.WriteLine("{0}加入{1}战队!", obs.Name, this.allyName);
players.Add(obs);
}
//注销方法--删除观察者--具体方法
public void Quit(IObserver obs) {
Console.WriteLine("{0}退出{1}战队!", obs.Name, this.allyName);
players.Remove(obs);
}
//声明抽象通知方法--通常通知方法在父类中实现,但是具体数据改动和通知方法的调用在子类中实现
public abstract void NotifyObserver(string name);
}
}
namespace ObserverSample{
/// 具体指挥中心--具体目标类
class ConcreteAllyControlCenter : AllyControlCenter{
public ConcreteAllyControlCenter(string allyName) {
Console.WriteLine("{0}战队组建成功!", allyName);
Console.WriteLine("----------------------------");
this.allyName = allyName;
}
//实现通知方法,此处的通知方法由观察者调用
public override void NotifyObserver(string name) {
Console.WriteLine("{0}战队紧急通知,盟友{1}遭受敌人攻击!", this.allyName, name);
//遍历观察者集合,调用每一个盟友(自己除外)的支援方法
foreach(object obs in players) {
if (!((IObserver)obs).Name.Equals(name)){ // 自己除外
((IObserver)obs).Help();
}
}
}
}
}
namespace ObserverSample{
/// 抽象观察者
interface IObserver{
string Name{
get;
set;
}
//声明支援盟友的响应方法--响应主题对象的通知-->被主题对象调用,用于接收其他队友被攻击的通知
void Help();
//声明遭受攻击方法--调用后转去主题对象,用于向其他所有队友发出通知
void BeAttacked(AllyControlCenter acc);
}
}
namespace ObserverSample{
/// 具体观察者
class Player : IObserver{
private string name;
public Player(string name){this.name = name;}
public string Name{
get { return name; }
set { name = value; }
}
//支援盟友方法的实现
public void Help(){Console.WriteLine("坚持住,{0}来救你!", this.name);}
//遭受攻击方法的实现,当遭受攻击时将调用战队控制中心类的通知方法NotifyObserver()来通知盟友
//需要引用具体主题对象
public void BeAttacked(AllyControlCenter acc) {
Console.WriteLine("{0}被攻击!", this.name);
acc.NotifyObserver(name);
}
}
}
namespace ObserverSample{
class Program{
static void Main(string[] args){
//定义观察目标对象
AllyControlCenter acc;
acc = new ConcreteAllyControlCenter("金庸群侠");
//定义四个观察者对象
IObserver player1, player2, player3, player4;
player1 = new Player("杨过");
acc.Join(player1);
player2 = new Player("令狐冲");
acc.Join(player2);
player3 = new Player("张无忌");
acc.Join(player3);
player4 = new Player("段誉");
acc.Join(player4);
//某成员遭受攻击
player1.BeAttacked(acc);
Console.Read();
}
}
}
执行结果:
金庸群侠战队组建成功!
----------------------------
杨过加入金庸群侠战队!
令狐冲加入金庸群侠战队!
张无忌加入金庸群侠战队!
段誉加入金庸群侠战队!
杨过被攻击!
金庸群侠战队紧急通知,盟友杨过遭受敌人攻击!
坚持住,令狐冲来救你!
坚持住,张无忌来救你!
坚持住,段誉来救你!
3.3 C#实现观察者模式
- C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托是存有对某个函数的引用的一种引用类型变量。引用可在运行时被改变。
- 委托特别用于实现事件和回调函数。所有的委托(Delegate)都派生自 System.Delegate 类。
委托就是一种引用方法的类型。可以看做是函数的抽象,是函数的“类”,委托的实例将代表一个具体的函数。
一个委托,可以搭载多个函数(多播),所有函数可以被依次唤起;委托对象所搭载的函数并不需要属于同一个类。
语法:delegate
例如:public delegate int MyDelegate (string s);
上例用于引用任何一个带有一个单一的 string 参数的函数,并返回一个 int 类型变量。
- 使用委托的前提条件:
委托对象所搭载的所有函数必须有相同的原型和形式,也就是拥有相同的参数列表和返回值类型。
委托可以看作是一个稳定的接口(充当抽象观察者角色)。 - 示例: DelegateObserver
namespace DelegateObserver{
class Program{
static void Main(string[] args){
//创建数据服务对象和观察者对象
BankAccount account = new BankAccount();
Email email = new Email("myemail@163.com");
Mobile mobile = new Mobile("1234567890");
//添加观察者--由客户端程序自主添加,各方法之间没有耦合,且允许方法的名称不同
account.Observers += email.SendEMail;
account.Observers += mobile.SendMessage;
//数据服务对象的更新方法中调用通知观察者
account.UpdateBalance(100);
account.Observers -= email.SendEMail;//删除观察者
Console.WriteLine("\n\n删除E-Mail通知以后:");
account.UpdateBalance(-50);
Console.Read();
}
}
}
namespace DelegateObserver{
/// 委托定义--指向处理余额通知的方法--委托相当于一个“类定义”--“类”中仅有一个方法--抽象观察者
/// <param name="val">余额值</param>
public delegate void AccountChangedHandler(double val);
/// 银行帐户--具体主题与抽象主题合并
class BankAccount{
/// 用于存储观察者方法的委托对象--此处使用的是多播委托(MulticastDelegate)--具体观察者列表
public AccountChangedHandler Observers = null; //初始值为null,需要客户端程序用"="或"+="来添加
/// 帐户余额
private double balance=0;
/// 存入/取出--调整余额
/// <param name="val">对帐户加减的金额</param>
public void UpdateBalance(double val){
balance += val;
if (Observers != null) //此处通过委托依次调用所有的观察者方法
Observers(balance);
}
}
}
namespace DelegateObserver{
/// 帐户余额发生变化时,发送Email通知用户
class Email{
/// 接收通知的邮件地址
private string EMailAddress;
public Email(string address){EMailAddress = address;}
/// 发送Email通知操作
/// <param name="val">余额数值</param>
public void SendEMail(double val){
Console.WriteLine("发送邮件到{0}, 余额为{1}.", EMailAddress, val);}
//其他操作相关的属性和方法
}
}
namespace DelegateObserver{
/// 帐户余额发生变化时,通过手机发送短信通知用户
class Mobile{
/// 接收短信通知的手机号码
private string PhoneNumber;
public Mobile(string phone_number){PhoneNumber = phone_number;}
/// 发送短信通知
/// <param name="val">余额数值</param>
public void SendMessage(double val){
Console.WriteLine("发送短信至手机{0},余额为{1}.", PhoneNumber, val);}
//其他操作相关的属性和方法
}
}
.NET中的事件处理模型是观察者模式的一种变形,它与观察者模式的实现原理本质上是一致的:
事件源对象充当主题角色,委托充当抽象观察者角色,事件处理对象充当具体观察者角色。如果事件源对象的某个事件触发,则调用事件处理对象中的事件处理程序来对事件进行处理。
示例:C#中的事件模拟ObserverExtend
示例代码:
namespace ObserverExtend{
//定义一个委托--抽象观察者
public delegate void UserInput(object sender, EventArgs e);
/// 服务方--事件的发行者-Subject(抽象主题和具体主题合二为一)
public class EventTest{
//定义一个此委托类型的事件--观察者列表
public event UserInput OnUserInput;
//模拟事件触发,当输入“0”时引发事件
public void Method(){
bool flag = false;
Console.WriteLine("请输入数字:");
while (!flag){ //--类似Windows的消息循环
if (Console.ReadLine() == "0")
{
OnUserInput(this, new EventArgs());
}
}
}
}
}
namespace ObserverExtend{
/// 客户端程序--具体观察者--同一个类可以包含多个观察者
public class Program{
public Program(EventTest test){
//注册事件或订阅事件--添加观察者
test.OnUserInput += new UserInput(Handler);
test.OnUserInput += new UserInput(HandlerMore);
//注销事件或取消订阅--取消观察者
//test.OnUserInput -= new UserInput(Handler);
}
/// 客户端测试函数-1--响应主题变化(通知)的方法--具体观察者
/// <param name="sender"></param>
/// <param name="e"></param>
public void Handler(object sender, EventArgs e){
Console.WriteLine("数据输入结束!");
}
/// 客户端测试函数-2--响应主题变化(通知)的方法--具体观察者
/// <param name="sender"></param>
/// <param name="e"></param>
public void HandlerMore(object sender, EventArgs e){
Console.WriteLine("真的结束了!");
}
static void Main(string[] args){
EventTest test = new EventTest();
Program program = new Program(test);
test.Method();
}
}
}
4. 观察者模式的总结
- 观察者模式使用面向对象的抽象,使得我们可以独立地改变数据服务者与观察者,从而使二者之间的依赖关系达到松耦合。
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。观察者自己决定是否需要订阅通知,目标对象对此一无所知。
- 在C#的实现方式中,委托定义充当了抽象的Observer接口,而管理委托对象的对象充当了目标对象。委托是比抽象Observer接口更为松耦合的设计----它不要求具体方法的名称相同,且同一个类可以充当多个观察者。
需要注意的问题:
- 如果一个被观察者对象(目标对象Subject)有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式时要特别注意这一点。
- 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎么发生变化的。