观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新。
注意:完整项目代码地址在文章末尾
借用设计模式head first书本中的例子,我们根据一次简单的项目设计来体现观察模式的妙处。
一家著名的气象站想要委托我们建立一个应用,应用中有三种布告板,分别显示目前的状况的布告板,气象统计布告板以及简单的预报布告板。
他们提供一个WeatherData给我们,当WeatherData对象获得最新的测量数据时,三种布告板需要实时更新。
而且,他们希望我们的应用是可扩展的,他们希望公布一组API供其他开发人员使用,好让其他开发人员写出自己的布告板,并插入此应用中。
首先,我们先看一下他们提供的WeatherData类源码:
package observerPattern.first;
import java.util.ArrayList;
import java.util.List;
public class WeatherData {
//温度
private float temperature;
//湿度
private float humidity;
//压强
private float pressure;
public WeatherData() {
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
measurementsChanged();
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
measurementsChanged();
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
measurementsChanged();
}
/**
* 此方法嵌入WeatherData中的set方法中,
* 只要数据发生改变,就会调用此方法
*/
public void measurementsChanged(){
//对方希望我们的实现在这儿
}
}
在进行设计之前,我们有必要自己模拟一个气象站,并将WeatherData与其绑定,否则后面的测试将令人头疼。
模拟气象站
建立一个类,其中组合了WeatherData对象,并写了一个start方法,其实现非常简单,就是建立三个线程分别随机的更新WeatherData中的属性达到模拟气象站的效果。
package observerPattern.first;
public class WeatherStation {
private WeatherData weatherData;
public WeatherStation(WeatherData weatherData){
this.weatherData = weatherData;
}
public void start(){
//快速创建线程的方式
new Thread(() -> {
while (true) {
try {
Thread.currentThread().sleep((long) (10000 * Math.random()));
weatherData.setHumidity((float) (100 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
try {
Thread.currentThread().sleep((long)(10000*Math.random()));
weatherData.setTemperature((float) (40*Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true) {
try {
Thread.currentThread().sleep((long) (10000 * Math.random()));
weatherData.setPressure((float) (100 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
下面对这个气象站测试一下:
package observerPattern.first;
public class WeatherTest {
public static void main(String[] args) throws InterruptedException {
//新建weatherData并与气象站绑定
WeatherData weatherData = new WeatherData();
new WeatherStation(weatherData).start();
while (true){
Thread.sleep(10000);
System.out.println(weatherData.toString());
}
}
}
准备工作完成后,我们现在分析下用户的需求,从上面的需求,我们可以提炼出三点:
1.需要建立三个布告板,分别显示目前的状况,气象统计以及简单的预报。
根据这个需求,我们可以简单的设计出三个类,分别代表三种不同的布告板:
目前状况布告板
package ObserverPatternStudy.first;
/**
* 目前状况布告板
*/
public class CurrentConditionsDisplay{
private float temp;
private float humidity;
private float pressure;
public void update(float temp,float humidity,float pressure){
this.temp=temp;
this.humidity = humidity;
this.pressure = pressure;
}
public void display(){
System.out.println("目前状况布告板:温度:"+temp+" 湿度:"+humidity);
}
}
气象统计布告板
package ObserverPatternStudy.first;
/**
* 天气统计布告板
*/
public class StatisticsDisplay{
private float temp;
private float humidity;
private float pressure;
public void update(float temp,float humidity,float pressure){
this.temp=temp;
this.humidity = humidity;
this.pressure = pressure;
}
public void display(){
System.out.println("气象统计布告板:温度:"+temp+" 湿度:"+humidity+" 压强:"+pressure);
}
}
天气预报布告板
package ObserverPatternStudy.first;
/**
* 天气预报预告版
*/
public class ForecastDisplay implements Display{
private float temp;
private float humidity;
private float pressure;
public void update(float temp,float humidity,float pressure){
this.temp=temp;
this.humidity = humidity;
this.pressure = pressure;
}
public void display(){
System.out.println("天气预报布告板:温度:"+temp+" 湿度:"+humidity+" 压强:"+pressure);
}
}
2.并根据用户提供的WeatherData与三个布告板绑定,当WeatherData的数据发生变化时,布告板要实时更新。
根据这个需求,我们需要对WeatherData源码进行修改,首先为了能在WeatherData数据发送变化时实时更新布告板,上面写的三种布告板必须组合进WeatherData中:
package ObserverPatternStudy.first;
import java.util.ArrayList;
import java.util.List;
public class WeatherData {
//温度
private float temperature;
//湿度
private float humidity;
//压强
private float pressure;
CurrentConditionsDisplay current;
StatisticsDisplay statis;
ForecastDisplay forecast;
public WeatherData(CurrentConditionsDisplay current ,StatisticsDisplay statis,
ForecastDisplay forecast) {
this.current = current;
this.statis = statis;
this.forecast = forecast;
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
measurementsChanged();
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
measurementsChanged();
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
measurementsChanged();
}
/**
* 此方法嵌入WeatherData中的set方法中,
* 只要数据发生改变,就会调用此方法
*/
public void measurementsChanged(){
current.update(getTemperature(),getHumidity(),getPressure());
statis.update(getTemperature(),getHumidity(),getPressure());
forecast.update(getTemperature(),getHumidity(),getPressure());
current.display();
statis.display();
forecast.display();
}
}
ok,设计完毕,但是有没有感觉不对劲呢? 我们是不是在面向实现编程? 这个不好啊。
我们应该面向抽象编程!! 当我们意识到这一点,我们会发现第三点需求原来如此简单。
3.用户希望WeatherData与布告板的绑定是可扩展的,也就是当有新的布告板时,不需要修改WeatherData的源码也能进行绑定。
好了,在第二步的基础上,我们对它进行抽象化来实现可扩展,首先我们新建一个Display接口:
package ObserverPatternStudy.first;
public interface Display {
void update(float temp,float humidity,float pressure);
void display();
}
然后让前面三个布告板都实现它:
public class CurrentConditionsDisplay implements Display
public class ForecastDisplay implements Display
public class StatisticsDisplay implements Display
接着修改WeatherData为如下:
package ObserverPatternStudy.first;
import java.util.ArrayList;
import java.util.List;
public class WeatherData {
//温度
private float temperature;
//湿度
private float humidity;
//压强
private float pressure;
List<Display> displayList;
public WeatherData() {
displayList = new ArrayList<>();
}
/**
* 添加布告板对象给WeatherData
* @param display
*/
public int addDisplay(Display display){
displayList.add(display);
return displayList.size()-1;
}
/**
* 删除指定订阅的布告板
* @param display
*/
public void removeDisplay(int display){
displayList.remove(display);
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
measurementsChanged();
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
measurementsChanged();
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
measurementsChanged();
}
/**
* 此方法嵌入WeatherData中的set方法中,
* 只要数据发生改变,就会调用此方法
*/
public void measurementsChanged(){
for(Display display:displayList){
display.update(getTemperature(),getHumidity(),getPressure());
display.display();
}
}
@Override
public String toString() {
return "WeatherData{" +
"temperature=" + temperature +
", humidity=" + humidity +
", pressure=" + pressure +
'}';
}
}
最后进行测试:
package ObserverPatternStudy.first;
public class WeatherTest {
public static void main(String[] args) throws InterruptedException {
//新建weatherData并与气象站绑定
WeatherData weatherData = new WeatherData();
new WeatherStation(weatherData).start();
//加入布告板
int currentDis = weatherData.addDisplay(new CurrentConditionsDisplay());
int foreDis= weatherData.addDisplay(new ForecastDisplay());
int staticDis = weatherData.addDisplay(new StatisticsDisplay());
}
}
做到这里我们也许能察觉到,我们已经通过这次需求在我们的设计中涉及到了一种模式:观察者模式。
我们再看下观察者模式定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新。
显然,WeatherData是一, 三个布告板是多,而且在WeatherData中的set方法中加入了通知执行方法,这也保证了三个布告板能及时收到更新。
但是,我们好像知道了一点,但又不完全知道,为什么呢? 因为没有规范设计。
上面的UML省略了DisplayElement(非必要元素)
首先,观察者模式分为两个角色,观察者和主题 ,分别对应两个接口:
主题
package ObserverPatternStudy.second;
public interface Subject {
//注册方法
void registerObserver(Observer observer);
//注销方法
void removeObserver(Observer observer);
//通知方法
void notifyObservers();
}
观察者
package ObserverPatternStudy.second;
public interface Observer {
//更新方法
void update(Object o);
}
所有的主题都应该实现Subject接口,所有的观察者都应该实现Observer接口。
我们很容易知道,WeatherData类对应主题, 布告板对应观察者。
于是我们可以编写如下代码:
WeatherData.java
package ObserverPatternStudy.second;
import java.util.ArrayList;
import java.util.List;
public class WeatherData implements Subject{
//温度
private float temperature;
//湿度
private float humidity;
//压强
private float pressure;
List<Observer> observers;
public WeatherData() {
observers = new ArrayList<>();
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
measurementsChanged();
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
measurementsChanged();
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
measurementsChanged();
}
/**
* 此方法嵌入WeatherData中的set方法中,
* 只要数据发生改变,就会调用此方法
*/
public void measurementsChanged(){
notifyObservers();
}
@Override
public String toString() {
return "WeatherData{" +
"temperature=" + temperature +
", humidity=" + humidity +
", pressure=" + pressure +
'}';
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
int i = observers.indexOf(observer);
if(i>=0) observers.remove(i);
}
@Override
public void notifyObservers() {
for(Observer observer:observers){
observer.update(this);
}
}
}
在编写布告板之前,我们需要定义一个接口,用来给布告板的显示行为抽象化:很简单
package ObserverPatternStudy.second;
public interface DisplayElement {
void display();
}
三种布告板如下:
这里只展示其中一个,其他的类似
package ObserverPatternStudy.second;
/**
* 最新天气布告板
*/
public class CurrentConditionDisplay implements Observer, DisplayElement {
//天气数据
private Subject weatherData;
private float temp;
private float humidity;
public CurrentConditionDisplay(Subject weatherData){
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("当前温度:"+temp+"\n"
+"当前湿度:"+humidity+"\n");
}
@Override
public void update(Object object) {
WeatherData weatherData = (WeatherData) object;
temp = weatherData.getTemperature();
humidity = weatherData.getHumidity();
display();
}
}
下面进行测试:
public class WeatherTest {
public static void main(String[] args) {
//新建weatherData并与气象站绑定
Subject weatherData = new WeatherData();
new WeatherStation((WeatherData) weatherData).start();
CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
}}
成功!!
如果你也复现出来了,那么你可以将这种实现和第一种实现对比一下,看一下第二种实现的优点在哪里。
另外:JDK也提供了对观察者模式编程的支持,但是也具有一定的局限性,建议感兴趣的同学用JDK的观察者模式的api再复现一边上面的需求。
下面让我们呢再巩固下观察者模式的定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新
项目地址:
https://gitee.com/yan-jiadou/design-mode/blob/master/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/src/main/java/ObserverPattern/DisplayTest.java