- 2.观察者模式
上一篇:《设计模式01——策略模式》
2.观察者模式
让你的对象知悉现状
观察者模式可以帮助对象知悉现况甚至在对象运行时可以决定是否要继续被通知,有了观察者模式,你的消息将会灵通
2.1观察者模式的构成:
出版者+订阅者=观察者
- 出版者称为主题
- 订阅者称为观察者
一旦数据改变,新的数据会以某种形式送到观察者手上
2.2 定义观察者模式
在真实的世界中,我们通常会发现观察者模式被定义为对象之间一对多的依赖,这样一来,当一个对象改变时,他的所有依赖者都会收到通知并自动更新
上面一张图可以看出主题和观察者之间定义了一对多的关系。观察者依赖于此主题,只要主题一改变,观察者就会被通知。
2.2.1 定义观察者模式:类图
2.3松耦合
两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此之间的细节。观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
对于观察者,主题只知道观察者实现了某个接口(Observer接口)。主题并不需要知道观察者的具体类是谁,做了其他什么细节。
而且在任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Obdserver接口的对象列表,所以3我们可以随时增加新的观察者。
2.3.1 设计原则
为了交互对象之间的松耦合设计而努力
松耦合设计之所以能够让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低
2.4 接下来我们通过气象站发布天气为例来分析该模式的用法
现有三个气象面板,分别显示的是当前的天气状况,天气预报,气象统计三个面板,同时气象站会实时的发布天气的数据(温度,湿度,气压)
2.4.1 建立主题接口
public interface Subject {
void registerObserver(Observer o);//用于注册观察者
void removeObserver(Observer o);//移除观察者
void notifyObservers();//通知观察者
}
2.4.2 建立观察者接口
/**
* 观察者--所有的气象组件都实现此观察者接口,
* 这样做的好处就是主题在要通知观察者时有了一个共同的接口
*/
public interface Observer {
/**
* 当气象观测值改变时,主题会把这些状态值当做方法参数传送给观察者
*
* @param temp
* @param humidity
* @param pressure
*/
void update(float temp, float humidity, float pressure);
}
2.4.3 建立显示接口
/**
* 该接口只包含一种方法也就是display,
*/
public interface DisplayElement {
void display();//当布告需要显示时调用此方法
}
在WeatherData中实现主题接口
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
/**
* 天气变化的类--即主题
*/
public class WeatherData implements Subject {
private ArrayList<Observer> observers;//我们加上一个Arraylist来记录观察者
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);//当观察者注册时,我们只要把它加在ArrayList后面即可
}
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);//移除观察者时我们也只需要从观察者队列中移除即可
}
}
@Override
public void notifyObservers() {
if (!CollectionUtils.isEmpty(observers)) {
for (Observer o : observers) {
o.update(temperature, humidity, pressure);
}
}
}
/**
* 更新气象数据
*/
public void measurementsChanged() {//当从气象站得到更新观测值时,我们需要通知观察者
this.notifyObservers();
}
/**
* 设置从气象站拿到的数据给到观察者
*
* @param temperature
* @param humidity
* @param pressure
*/
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
this.measurementsChanged();//执行通知操作
}
}
2.4.4 建立布告板
2.4.4.1 当前天气布告板
/**
* 布告板--当前天气状况的布告板
* 把最近的温度湿度显示出来
*/
public class CurrentConditionDisplay implements Observer, DisplayElement {
private float tempature;//温度
private float humidity;//湿度
private Subject weatherData;//由于此布告板实现了Observer接口,所以可以从WeatherData中获取改变
public CurrentConditionDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);//构造函数首先将自身添加到观察者行列中
}
@Override
public void display() {
System.out.println("当前温度:【" + this.tempature + "F degree】\n当前湿度:【" + this.humidity + "% humidity】");
}
@Override
public void update(float temp, float humidity, float pressure) {
this.tempature = temp;
this.humidity = humidity;
this.display();
}
}
2.4.4.2 天气预报布告板
/**
* 布告板--天气预报布告板
* 分析当天的平均气温,最高温度以及最低温度
*/
public class ForecastDisplay implements Observer, DisplayElement {
private float currentPressure = 29.2f;//当前压力值
private float lastPressure;//最终压力值
private Subject weatherData;//由于此布告板实现了Observer接口,所以可以从WeatherData中获取改变
public ForecastDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);//构造函数首先将自身添加到观察者行列中
}
@Override
public void display() {
System.out.print("天气预报: ");
if (currentPressure > lastPressure) {
System.out.println("Improving weather on the way!");
} else if (currentPressure == lastPressure) {
System.out.println("More of the same");
} else if (currentPressure < lastPressure) {
System.out.println("Watch out for cooler, rainy weather");
}
}
@Override
public void update(float temp, float humidity, float pressure) {
this.lastPressure = this.currentPressure;
currentPressure = pressure;
this.display();
}
}
2.4.4.3 建立气象分析布告板
/**
* 布告板--气象分析布告
* 分析当天的平均气温,最高温度以及最低温度
*/
public class StatisticsDisplay implements Observer, DisplayElement {
private float maxTemp = 0.0f;//最高气温
private float minTemp = 200;//最低气温
private float tempSum = 0.0f;//当前气温
private int numReadings;
private Subject weatherData;//由于此布告板实现了Observer接口,所以可以从WeatherData中获取改变
public StatisticsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
+ "/" + maxTemp + "/" + minTemp);
}
@Override
public void update(float temp, float humidity, float pressure) {
numReadings++;
if (temp > maxTemp) {
maxTemp = temp;
}
if (temp < minTemp) {
minTemp = temp;
}
display();
}
}
2.4.5 启动气象站
主运行类
/**
* 气象站
*/
public class WeathStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
HeatIndexDisplay heatIndexDisplay = new HeatIndexDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
}
}
运行结果如下:
如果我们还需要其他布告板可以根据具体的需求参考上面建立布告板的方法去创建,比如我们在临时加一个关于酷热指数的布告板来显示人的温度感受
2.4.6 酷热指数布告板
/**
* 布告板--酷热指数
* 结合温度和湿度的指数--用于显示人的温度感受
*/
public class HeatIndexDisplay implements Observer, DisplayElement {
private float heatIndex = 0.0f;//酷热指数
private Subject weatherData;//由于此布告板实现了Observer接口,所以可以从WeatherData中获取改变
public HeatIndexDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);//将自身注册为观察者
}
@Override
public void display() {
System.out.println("当前的酷热指数为:" + heatIndex);
}
@Override
public void update(float temp, float humidity, float pressure) {
this.heatIndex = this.computeHeatIndex(temp, humidity);
this.display();
}
/**
* 计算酷热指数
*
* @param t
* @param rh
* @return
*/
private float computeHeatIndex(float t, float rh) {
float index = (float) ((16.923 + (0.185212 * t) + (5.37941 * rh) - (0.100254 * t * rh) +
(0.00941695 * (t * t)) + (0.00728898 * (rh * rh)) +
(0.000345372 * (t * t * rh)) - (0.000814971 * (t * rh * rh)) +
(0.0000102102 * (t * t * rh * rh)) - (0.000038646 * (t * t * t)) + (0.0000291583 *
(rh * rh * rh)) + (0.00000142721 * (t * t * t * rh)) +
(0.000000197483 * (t * rh * rh * rh)) - (0.0000000218429 * (t * t * t * rh * rh)) +
0.000000000843296 * (t * t * rh * rh * rh)) -
(0.0000000000481975 * (t * t * t * rh * rh * rh)));
return index;
}
}
在主运行程序里进行测试
HeatIndexDisplay heatIndexDisplay = new HeatIndexDisplay(weatherData);
运行结果如下:
2.5 java内置的观察者模式
接下来我们还是通过气象站发布天气为例来分析该模式的用法,但是这次我们不是通过自己手动去建立观察者和主题,通过使用java内置的观察者模式来进行研究
java内部的观察者模式分为:
- java.util.Observer 观察者
- java.util.Observable 主题(也叫被观察者)
2.5.1 java内置的观察者模式如何运作
2.5.1.1 如何把对象变为观察者
如同以前一样,实现观察者接口,然后调用任何Observable对象的addObserver()方法。不想在当观察者时,调用deleteObserver()方法就可以了。
2.5.1.2 主题如何推送通知
首先我们需要利用扩展的java.util.Observalbe接口产生“可观察者”类,然后需要两个步骤
- 1.先调用setChanged()方法,标记状态已经改变的事实。
setChanged()方法用来标记状态已经改变的事实,好让notifyObservers()知道当它被调用时应该更新观察者。如果调用notifyObservers()之前没有先调用setChanged()方法,观察者就不会被通知。
Observalbe的内部源码如下:
若没有setChanged()方法,气象站测量如此之敏锐,以至于温度计数每十分之一度就会被更新一次,这就会造成WeatherData对象持续不断的通知观察者,但是这种情况是不会被允许的,应该度数变化在半度以上才需要通知观察者,所以我们就可以在温度变化大于半度时调用setChanged()方法,进行有效的更新。
- 2.然后调用两种notifyObservers()方法中的一个:
- notifyObservers() 或
- notifyObservers(Object arg)
2.5.1.3 观察者如何接收通知
同以前的方法一样,观察者实现了更新的方法,但是方法的签名略有不同:
update(Observerable o,Object arg);
- 主题本身当做第一个变量,好让观察者知道是哪个主题通知了它
- 第二个参数是传入的notifyObservers的数据对象,若没有则说明为空
2.5.1.4 将WeatherData改造为java.util.Observable
import java.util.Observable;
/**
* 构造一天天气的主题 继承Observable类
*/
public class WeatherDataInner extends Observable {
private float tempature;
private float humidity;
private float pressure;
public WeatherDataInner() {//与我们自定义的主题相比,我们的构造器已经不需要为了记住观察者而建立数据结构了
}
/**
*
*/
public void measurementsChanged() {
setChanged();//在调用notifyObservers方法之前需要先调用setChanged方法来指定状态已经改变
notifyObservers();//我们没有调用notifyObservers方法来传递对象,这表示我们采用的做法是"拉"
}
public void setMeasurements(float tempature, float humidity, float pressure) {
this.tempature = tempature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTempature() {
return tempature;
}
public void setTempature(float tempature) {
this.tempature = tempature;
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
}
}
2.5.1.5 重做CurrentConditionDisplay
import java.util.Observable;
import java.util.Observer;
public class CurrentConditionsDisplayInner implements Observer, DisplayElement {
Observable observable;
private float temperature;
private float humidity;
public CurrentConditionsDisplayInner(Observable observable) {//构造函数的作用就在于将CurrentConditionsDisplayInner登记为观察者
this.observable = observable;
observable.addObserver(this);
}
@Override
public void display() {
System.out.println("当前温度:【" + this.temperature + "F degree】--当前湿度:【" + this.humidity + "% humidity】");
}
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherDataInner) {//首先确定观察者属于WeatherData类型的,然后利用setter方法获取温度和湿度测量值,最后调用display
WeatherDataInner weatherData = (WeatherDataInner) o;
this.temperature = weatherData.getTempature();
this.humidity = weatherData.getHumidity();
this.display();
}
}
}
2.5.1.6 重做ForecastDisplay
import java.util.Observable;
import java.util.Observer;
/**
* 布告板--天气预报布告板
* 分析当天的平均气温,最高温度以及最低温度
*/
public class ForecastDisplayInner implements Observer, DisplayElement {
Observable observable;
private float currentPressure = 29.2f;//当前压力值
private float lastPressure;//最终压力值
public ForecastDisplayInner(Observable o) {
this.observable = o;
o.addObserver(this);
}
@Override
public void display() {
System.out.print("天气预报: ");
if (currentPressure > lastPressure) {
System.out.println("Improving weather on the way!");
} else if (currentPressure == lastPressure) {
System.out.println("More of the same");
} else if (currentPressure < lastPressure) {
System.out.println("Watch out for cooler, rainy weather");
}
}
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherDataInner) {
WeatherDataInner weatherDataInner = (WeatherDataInner) o;
this.lastPressure = this.currentPressure;
this.currentPressure = weatherDataInner.getPressure();
this.display();
}
}
}
在主运行类中进行测试
/**
* 气象站
*/
public class WeathStation {
public static void main(String[] args) {
// WeatherData weatherData = new WeatherData();
// CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
// ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
// HeatIndexDisplay heatIndexDisplay = new HeatIndexDisplay(weatherData);
// StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
// weatherData.setMeasurements(80, 65, 30.4f);
/**java内部观察者对象测试**/
WeatherDataInner weatherDataInner = new WeatherDataInner();
CurrentConditionsDisplayInner currentConditionsDisplayInner = new CurrentConditionsDisplayInner(weatherDataInner);
ForecastDisplayInner forecastDisplayInner = new ForecastDisplayInner(weatherDataInner);
weatherDataInner.setMeasurements(80, 65, 30.4f);
}
}
运行结果:
从运行结果我们发现有个问题就是运行的顺序是不同的,所以我们不能依赖于观察者的被通知次序
2.6 观察者模式的设计原则
-
1.找出程序中变化的方面,然后将其和固定不变的方面的地方相分离
在观察者模式中,会改变的是主题的状态以及观察者的数目和类型。用这个模式,我们可以改变依赖于主题状态的对象,却不必改变主题,这就叫提前规划。
-
2.针对接口编程而不是针对实现编程
主题与观察者都使用接口,观察者利用主题的接口向主题注册,而主题利用观察者的接口通知观察者,这样可以让两者之间运作正常,又同时具有松耦合的优点
-
3.多用组合少用继承
观察者模式利用“组合”将许多观察者组合进主题中。对象之间的这种关系不是通过继承产生的,而是在运行期间利用组合的方式产生的。
至此观察者模式学习结束。
下一篇:《设计模式03—装饰者模式》