观察者模式
一、引子—气象站监测应用的概况
系统分三个部分
气象站:获取实际气象数据的物理装置
WeatherData对象:追踪来自气象站的数据,并更新布告板。
布告板:显示目前天气状况给用户。包括目前状况、气象统计、天气预报。
这个项目要完成的任务是:利用WeatherData对象取得数据,并更新三个布告板。
二、WeatherData类
rel="File-List" href="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_filelist.xml" />
Class WeatherData
{
getTemperature()
getHumidity()
getPressure()
measurementChanged()
}
rel="File-List" href="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_filelist.xml" />
1.WeatherData具有getter方法,可以取得三个测量值:温度、湿度和气压。我们不在乎这些值“如何”被设置,WeatherData对象自己知道如何从气象站取得更新数据。
2.当新数据备妥时,measurementChanged()方法会被调用(我们不在乎次方法是如何调用,只在乎它被调用了)。
3.我们需要实现三个使用天气数据的布告板:目前状况、气象统计、天气预报。
4.此系统可以扩展,让其他开发人员建立定制的布告板,用户可以随心所欲地添加或删除任何布告板。
二、错误的示范
在measurementChanged()方法中添加我们的代码:
public class WeatherData
{
public void measurementChanged()
{
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
//调用每个布告板更新显示,出入最新的测量。
//但是,针对具体实现编程,会导致我们以后增加或删除布告板时必须修改程序。
//这是改变的地方,需要封装起来。
//不过,至少这里看起来像是一个统一的接口。
//布告板的方法名称都是update(),参数都相同。
currentConditionsDisplay.update(temp,humidity,pressure);
statisticsDisplay.update(temp,humidity,pressure);
forecastDisney.update(temp,humidity,pressure);
}
}
三、认识观察着模式
报纸杂志的订阅
1.报社的业务就是出版报纸(主题subject)
2.向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸(注册为观察者observer之后,主题就会不断地向观察者发送最新信息)
3.当你不想再看报纸时,取消订阅,他们就不会再送新报纸(观察者取消注册,不再是观察者,主题不再发送信息)
4.只要报社还在运行,就会一直有人向他们订阅报纸或取消订阅。(可以随时注册成观察者,或者取消注册).
出版者+订阅者=观察者模式
四、定义观察者模式及类图
观察者模式:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都会受到通知并自动更新。
rel="File-List" href="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_filelist.xml" />
类图
rel="File-List" href="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_filelist.xml" />
五、松耦合的威力
观察者模式提供了一种对象设计,让主题和观察者之间松耦合。为什么呢?
关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主题不需要知道观察者的具体类是谁、做了些什么或其他任何细节。
任何时候我们都可以增加新的观察者。因为注意唯一依赖的是一个实现Observer接口的对象列表,事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样,也可以随时删除某些观察者。
有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当观察者,我不不需要为了兼容新类型而修改主题的代码,所要做的就是在新的类里实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所有实现了观察者接口的对象。
我们可以独立地复用主题或观察者,因为二者并非紧耦合。
改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间的接口仍被遵守,我们就可以自由第改变它们。
设计原则:为了交互对象直接的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的相互依赖降到了最低。
六、类图
1.主题类
rel="File-List" href="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_filelist.xml" />
2.观察者类
3. 气象站类图
七、源码
观察者模式推push的方式
由主题主动将数据送给观察者。
拉pull的方式
主题提供一些公开的getter方法,让观察者“拉”走需要的数据。
辩论:
1.主题可以让观察者拉走数据,但是观察者可能需要调用很多次才能收集齐全所要的数据,而推的方式是主题把自己的数据全部传送给观察者,观察者可以在一次通知中得到所有状态数据。
2.但是,观察者种类众多,主题不可能事先料到所要观察者的不同需求。观察者直接去拉走需要的数据,这样一来,如果观察者需要一点点数据,就不会强迫受到一堆数据了。同时,也便于以后扩展。如果主题决定增加更多的状态数据,就不用修改和更新对每个观察者的调用,只需改变自己允许更多的getter方法来取得新增的装态。
1.推push的方式
ISubject.cs
interface ISubject
{
//这两个方法都需要观察着做参数,该观察者是用来被注册或删除的
void registerObserver(IObserver o);
void removeObserver(IObserver o);
//当主题状态改变时,这个方法被调用,通知所有观察者
void notifyObserver();
}
WeatherData.cs
class WeatherData:ISubject
{
//加上一个ArrayList来记录观察者,此ArrayList在构造器中建立。
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData()
{
observers = new ArrayList();
}
#region ISubject Members
//当注册观察者时,我们只要把它加到ArrayList的后面即可
public void registerObserver(IObserver o)
{
observers.Add(o);
}
//观察者取消注册,我们把它从ArrayList中删除即可
public void removeObserver(IObserver o)
{
int i = observers.IndexOf(o);
if(i>=0)
{
observers.Remove(o);
}
}
//此处,我们把状态告诉每一个观察者。
//因为观察者都实现了update(),所有我们知道如何通知他们.
public void notifyObserver()
{
foreach(IObserver observer in observers)
{
observer.update(temperature, humidity,pressure);
}
}
#endregion
//从气象站得到更新观测值时,我们通知观察者。
public void measurementsChanged()
{
notifyObserver();
}
public void setMeasuremensts(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
IObserver.cs
//所有观察者都必须实现update方法,以实现观察者接口。
interface IObserver
{
//当气象观测值改变时,主题会把这些状态当作方法的参数,传送给观察者
void update(float temp, float humidity, float pressure);
}
IDisplayElement.cs
//DisplayElement接口只包含了一个display方法,
//当布告板需要显示时,调用此方法
interface IDisplayElement
{
void display();
}
StatisticsDisplay.cs
class StatisticsDisplay:IObserver,IDisplayElement
{
#region Members
private float maxTemp = 0.0f;
private float minTemp = 200;//set intial high so that minimum
//is set first invokation
private float temperatureSum = 0.0f;
private int numReadings = 0;
private ISubject weatherData;
#endregion//Members
#region NumberOfReadings Property
public int NumberOfReadings
{
get
{
return numReadings;
}
}
#endregion//NumberOfReadings Property
#region Constructor
public StatisticsDisplay(ISubject weatherData)
{
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
#endregion//Constructor
#region IObserver Members
public void update(float temperature, float humidity, float pressure)
{
temperatureSum += temperature;
numReadings++;
if (temperature > maxTemp)
{
maxTemp = temperature;
}
if (temperature < minTemp)
{
minTemp = temperature;
}
display();
}
#endregion
#region IDisplayElement Members
public void display()
{
float average = temperatureSum / numReadings;
System.Console.WriteLine("Statistics:Avg/Max/Min temperature = {0}F,{1}F,{2}F",
average, maxTemp, minTemp);
}
#endregion
}
CurrentConditionsDisplay.cs
class CurrentConditionsDisplay:IObserver, IDisplayElement
{
private float temperature;
private float humidity;
private ISubject weatherData;
public CurrentConditionsDisplay(ISubject weatherData)
{
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
#region IObserver Members
public void update(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
//this.pressure = pressure;
display();
}
#endregion
#region IDisplayElement Members
public void display()
{
System.Console.WriteLine( "Current conditions: {0}F degrees and {1}% humidity" ,
temperature,humidity );
}
#endregion
}
ForcastDisplay.cs
class ForcastDisplay : IObserver, IDisplayElement
{
private float currentPressure = 29.92f;
private float lastPressure;
private ISubject weatherData;
public ForcastDisplay(ISubject weatherData)
{
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
#region IObserver Members
public void update(float temperature, float humidity, float pressure)
{
lastPressure = currentPressure;
currentPressure = pressure;
display();
}
#endregion
#region IDisplayElement Members
public void display()
{
string str="Forcast:";
if(currentPressure > lastPressure)
{
str += "Improving weather on the way!";
}
else if (currentPressure == lastPressure)
{
str += "More of the same";
}
else if (currentPressure < lastPressure)
{
str += "Watch out for cooler, rainy weather";
}
System.Console.WriteLine(str);
}
#endregion
}
Program.cs
class Program
{
static void Main(string[] args)
{
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
ForcastDisplay forcastDisplay = new ForcastDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasuremensts(80, 65, 30.4F);
}
}
rel="File-List" href="file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/msohtml1/01/clip_filelist.xml" />
2.拉pull的方式
以WeatherData.cs和CurrentConditionsDisplay.cs为例进行说明
WeatherData.cs
class WeatherData:ISubject
{
//加上一个ArrayList来记录观察者,此ArrayList在构造器中建立。
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData()
{
observers = new ArrayList();
}
#region ISubject Members
//当注册观察者时,我们只要把它加到ArrayList的后面即可
public void registerObserver(IObserver o)
{
observers.Add(o);
}
//观察者取消注册,我们把它从ArrayList中删除即可
public void removeObserver(IObserver o)
{
int i = observers.IndexOf(o);
if(i>=0)
{
observers.Remove(o);
}
}
//此处,我们把状态告诉每一个观察者。
//因为观察者都实现了update(),所有我们知道如何通知他们.
public void notifyObserver()
{
foreach(IObserver observer in observers)
{
observer.update();
}
}
#endregion
//从气象站得到更新观测值时,我们通知观察者。
public void measurementsChanged()
{
notifyObserver();
}
public void setMeasuremensts(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature()
{
return temperature;
}
public float getHumidity()
{
return humidity;
}
public float getPressure()
{
return pressure;
}
}
CurrentConditionsDisplay.cs
class CurrentConditionsDisplay:IObserver, IDisplayElement
{
private float temperature;
private float humidity;
private ISubject weatherData;
public CurrentConditionsDisplay(ISubject weatherData)
{
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
#region IObserver Members
public void update()
{
this.temperature = ((WeatherData)weatherData).getTemperature();
this.humidity = ((WeatherData)weatherData).getHumidity();
display();
}
#endregion
#region IDisplayElement Members
public void display()
{
System.Console.WriteLine( "Current conditions: {0}F degrees and {1}% humidity" ,
temperature,humidity );
}
#endregion
}
八、设计箱内的工具
OO原则:
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之间的松耦合设计而努力
观察者模式:
在对象之间定义一对多的依赖,这样一来当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。
要点:
观察者模式定义了对象之间一对多的关系。
主题用一个共同的接口来更新观察者。
观察者和主题之间用松耦合方式结合,主题不知道观察者的细节,只知道观察者实现了观察者接口。
使用此模式时,可从主题处推 push 或拉 pull 数据(推的方式被认为更“正确”)