本文主要是记录《Head First 设计模式》知识,目的是检查自己学到的知识,同时方便我以后进行复习和浏览。
一、概述
1-1 定义
观察者模式 Observer Pattern:Define a one-to-many dependency between object so that when one object changes state,all its dependents are notified and updated automatically.
定义对象间的一种一对多的依赖关系,每当一个对象的状态发生改变时,所有依赖于它的对象都可以得到通知并被自动更新。也称为发布-订阅(Publish/Subscribe)模式、模型-视图(Model-View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式
该模式是一种行为模式
1-2 模式结构
观察者模式包含如下角色:
- Subject : 目标/主题。对象使用此接口注册为观察者,或者把自己从观察者中删除。
- ConcreteSubject : 具体目标/主题。实现主题接口,除了注册和撤销方法,具体还实现了notityObserver()方法,此方法用于在状态改变时更新所有观察者。
- Observer : 观察者。此接口只有一个update()方法,当主体状态改变时它被调用。
- ConcreteObserver:具体观察者。观察者必须注册具体实体,以便接收更新。
1-3 模式动机
建立一种对象与对象之间的依赖,当一个对象发生改变时,会自动通知其依赖于该对象的所有对象,这些对象会作出相应反应。其中改变的对象称为主体,而被通知的对象称为观察者,一个观察目标(观察者接口)对应多个观察者,观察者之间没有互相联系,观察者本身可以根据需要决定是否注册和删除,使系统更易于扩展(具体观察者与具体目标/主题)。
二、举例:
该观察者模式存在两种情况:
- 推模式 Push:当主题的状态发生改变时,主题会将消息以参数形式主动推送给观察者,不管观察者是否需要。
- 拉模式 Pull:通知消息的方法本身并不带任何参数,是由观察者自己到主题对象那取回数据。
2-1 应用概述
气象监控应用:由WeatherData对象负责追踪目前的天气状况(温度、湿度、气压)。建立一种应用,有三种布告板,分别为目前的状态、气象统计及简单的预报。当WeatherData对象获取到最新值时,三种布告板必须实时更新。Weather-O-Rama希望公布一组API,好让其他开发人员可以写出自己的布告板,并插入此应用中。
这是气象台送来的WeatherData源文件:
目前我们知道些什么:
- WeatherData 类具有getting方法,可以取得三个测量值:温度、湿度、气压.
- 当新的测量数据备妥时,measurementsChanged()方法就会被条用。(不在如何被条用,只在乎他被调用)
- 需要实现三个使用天气数据的布告板:“目前状况”、“气象统计”、“天气预报”布告。一旦WeatherData有新的测量,这些布告必须马上更新。
- 此系统可扩展,用户可以随心所欲地添加或删除任何布告板。目前初始的布告板有三类:“目前状态”布告、“气象统计”布告、“天气预报”布告
2-2 推模式
推模式代码
主题代码:
///
/// 主题接口
///
public interface ISubject
{
///
/// 注册观察者
///
///
public void RegisterObserver(IObserver O);
///
/// 删除观察者
///
///
public void RemoveObserver(IObserver O);
///
/// 主题改变时,通知观察者
///
public void NotifyObserver();
}
具体主题代码:
public class WeatherData : ISubject
{
private List observers;
private float Temperature;
private float Humidity;
private float Pressure;
///
/// WeatherData数据改变
///
///
///
///
public void setMeasurements(float temperature, float humidity, float pressure)
{
this.Temperature = temperature;
this.Humidity = humidity;
this.Pressure = pressure;
measurementsChanged();
}
public void measurementsChanged()
{
NotifyObserver();
}
public WeatherData()
{
observers = new List();
}
///
/// 通知所有绑定的观察者
///
public void NotifyObserver()
{
foreach (var item in observers)
{
item.Update(Temperature, Humidity, Pressure);
}
}
///
/// 注册观察者
///
///
public void RegisterObserver(IObserver O)
{
observers.Add(O);
if (observers.Contains(O))
{
Console.WriteLine("注册观察者成功");
}
else
{
Console.WriteLine("注册观察者失败");
}
}
///
/// 移除观察者
///
///
public void RemoveObserver(IObserver O)
{
observers.Remove(O);
if (observers.Contains(O))
{
Console.WriteLine("移除观察者失败");
}
else
{
Console.WriteLine("移除观察者成功");
}
}
///
/// 获取温度
///
///
public float getTmperature()
{
return Temperature;
}
///
/// 获取湿度
///
///
public float getHumidity()
{
return Humidity;
}
///
/// 获取气压
///
///
public float getPressure()
{
return Pressure;
}
}
布告板有共同的方法Display,所以提出来布告板相同的部分
///
/// 布告板接口
///
interface IDisplayElement
{
public void Display();
}
观察者代码:
///
/// 观测者接口
///
public interface IObserver
{
///
/// 改变接口
///
///
///
///
public void Update(float temp,float humidity,float pressure);
}
具体观察者代码:
///
/// 目前状况布告板
///
public class CurrentConditionsDisplay : IObserver, IDisplayElement
{
private float Temperature;
private float Humidity;
private float Pressure;
private ISubject WeartherData;
///
/// 构造函数
///
///
public CurrentConditionsDisplay(ISubject weatherData)
{
this.WeartherData = weatherData;
weatherData.RegisterObserver(this);
}
///
/// 展示
///
public void Display()
{
Console.WriteLine("Current conditions:{0}F 、{1}%humidity and {2}pressure", Temperature, Humidity, Pressure);
}
///
/// 改变
///
///
///
///
public void Update(float temp, float humidity, float pressure)
{
this.Temperature = temp;
this.Humidity = humidity;
this.Pressure = pressure;
Display();
}
}
测试程序代码:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("推模式");
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay current = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(85, 55, 33.4f);
weatherData.setMeasurements(75, 43, 27.4f);
}
}
运行结果:
2-3拉模式
主题如何送出通知
① 先调用setChanged()方法,标记状态已经改变的事实
② 然后调用两种NotifyObservers()方法中的一个:NotifyObservers()或NotifyObservers(object arg)
观察者如何接受通知
同以前一样,观察者实现更新的方法,但签名不太一样:Update(ISubject sub,object arg)
拉模式代码
拉模式与推模式有相同代码,所以只贴出来不同部分的代码。
具体主题代码:
class WeatherData : ISubject
{
private List observers;
private float Temperature;
private float Humidity;
private float Pressure;
private bool changed = false;
///
/// 构造函数
///
public WeatherData()
{
observers = new List();
}
///
/// 主题状态改变,调用该方法
///
///
///
///
public void setMeasurements(float temperature, float humidity, float pressure)
{
this.Temperature = temperature;
this.Humidity = humidity;
this.Pressure = pressure;
measurementsChanged();
}
public void measurementsChanged()
{
SetChanged();
NotifyObserver();
}
///
/// 查看是否修改状态
///
public void SetChanged()
{
changed = true;
}
///
/// 通知所有绑定的观察者
///
/// 传入NotityObserver()的数据对象,如果没有说明则为空
public void NotifyObserver(object arg)
{
if (changed)
{
foreach (var item in observers)
{
item.Update(this, arg);
}
changed = false;
}
}
///
/// 通知所有绑定的观察者
///
public void NotifyObserver()
{
NotifyObserver(null);
}
///
/// 注册观察者
///
///
public void RegisterObserver(IObserver O)
{
observers.Add(O);
if (observers.Contains(O))
{
Console.WriteLine("注册观察者成功");
}
else
{
Console.WriteLine("注册观察者失败");
}
}
///
/// 移除观察者
///
///
public void RemoveObserver(IObserver O)
{
observers.Remove(O);
if (observers.Contains(O))
{
Console.WriteLine("移除观察者失败");
}
else
{
Console.WriteLine("移除观察者成功");
}
}
///
/// 获取温度
///
///
public float getTmperature()
{
return Temperature;
}
///
/// 获取湿度
///
///
public float getHumidity()
{
return Humidity;
}
///
/// 获取气压
///
///
public float getPressure()
{
return Pressure;
}
}
观察者代码:
///
/// 拉模式的观察者 接口
///
public interface IObserver
{
///
/// 改变接口
///
public void Update(ISubject sub, Object arg);
}
具体观察者代码:
///
/// 目前状况布告板
///
class CurrentConditionsDisplay : IObserver, IDisplayElement
{
ISubject Subject;
private float Temperature;
private float Humidity;
///
/// 构造函数
///
/// 主题
public CurrentConditionsDisplay(ISubject sub)
{
this.Subject = sub;
Subject.RegisterObserver(this);
}
public void Display()
{
Console.WriteLine("Current conditions:{0}F 、{1}%humidity ", Temperature, Humidity);
}
///
/// 修改部分
///
/// 主题
///
public void Update(ISubject sub, object arg)
{
if (sub is WeatherData)
{
WeatherData weather = (WeatherData)sub;
this.Temperature = weather.getTmperature();
this.Humidity = weather.getHumidity();
}
Display();
}
}
测试程序代码:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("推模式");
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay current = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(85, 55, 33.4f);
weatherData.setMeasurements(75, 43, 27.4f);
}
}
运行结果:
三 总结
3-1 模式优缺点
观察者模式优点
- 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
- 观察者模式在观察目标与观察者(Oserver)建立一个抽象耦合。
- 观察者模式支持广播通信
- 观察者模式符合"开-闭原则"
观察者模式缺点
- 如果一个观察目标有很多直接和简介观察者,通知所有的观察者会花费很多的时间。
- 如果观察目标和观察者之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标是怎么发生变化,仅仅知道观察目标发生了变化。
观察者模式推优点是实时、高效。缺点是:观察者会收到主题传送的内部状态;当观察者种类比较多,主题维护观察者比较麻烦;当观察者只需要一点数据是,会被迫收到一堆数据。
观察者模式拉优点:如果观察者众多,会将订阅关系放在Observer;观察者自行决定获取数据;当扩展功能时(增加更多的状态),只需改变自己的getting方法。缺点是:主题会暴露我们不想暴露的内部成员
3-2 适合场景
以下情况可以使用观察者模式:
- 一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这些方面封装在独立的对象中使它们可以独立地改变和复用。
- 一个对象的改变将导致一个或多个对象进行改变,不知道具体多少对象将发生改变,可以降低系统的耦合度。
- 一个对象必须通知其他对象,而并不知道这些对象是谁。
- 需要在系统中创建一个触发链,A对象改变通知B对象,B对象的行为影响C对象......,可以使用观察者模式创建一个触发链。
3-3 模式应用
观察者模式使用非常广泛,凡是涉及一对一或者一对多的对象都可以使用观察者模式,例子:某购物网站执行发送后将打折信息发送给用户,某手机软件有活动点击发送后通知所有用户。
总结
- 观察者模式定义了对象之间一对多的关系
- 主题(也就是可观察者)用一个共同的接口来更新观察者
- 观察者与可观察者之间用松耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。
- 使用此模式,你可以使用被观察者处推(Push)或拉(Pull)数据(然而,推的方式认为更正确)。
- 有多个观察者时,不可以依赖特定的通知次序。