观察者模式在实际开发中是一个使用率极高的一种设计模式,在设计模式中相比较来说也是必须要掌握的模式之一,观察者模式(有时又被称为发布(publish)- 订阅者(Subscribe)模式、模型 - 视图(View)模式等)是软件设计的一种模式,在此种模式中,一个目标物件管理所有相依赖于它的观察者物件,并且在它本身的状态发生改变时主动发出通知,此种模式通常来实现事件处理系统
一、观察者模式基本介绍
观察者模式(Observer)完美的将观察者和被观察者的对象分离开,举个例子,用户界面可以作为一个观察者,业务数据则是被观察者,用户界面观察业务数据的变化,发现数据变化后,就显示在界面上,面向对象的设计的一个原则是:系统中的每个类将重点放在某一个功能上,而不是其他方面,一个对象只做一件事情,并且将它做好,观察者模式在模块之间划定了清晰的界限,提高了应用程序的可维护性和重用性
二、观察者模式的定义
定义对象间的一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新
三、观察者模式的使用场景
当你的项目中有多对一的依赖关系,可以考虑使用,因为观察者模式就是针对对象之间多对一依赖的一种设计方案,被依赖的对象为 Subject,依赖的对象为 Observer,Subject 通知 Observer 变化
四、观察者模式的模型图
1. Subject: 抽象主题,也就是被观察者(Observerable)的角色,抽象主题角色把所有观察者对象的引用保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象
2. ConcreteSubject: 具体主题,该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发出通知,具体主题角色又叫做具体被观察者(ConcreteObserverable)角色
3. Observer: 抽象观察者,该角色是观察者的抽象类,它定义了一个更新接口,使得在得到主题的更改通知时更新自己
4. ConcreteObserver: 具体观察者,该角色实现抽象观察者角色所定义的更新接口,以便在主题的状态发生变化时更新自身的状态
五、观察者模式实例演示
前几天阅读《Head First》一书学习观察者模式时觉得里面举的例子就挺好,这里我们暂且也借用里面的例子来演绎,我们从从一个项目的需求实际出发,从普通的实现方法和用观察者模式实现的不同角度来对比在一些特定的场景模式下,使用观察者模式解决相应问题的方便的可扩展性以及便捷性
需求:加入我们的团队刚刚接到一份合约,负责建立一个气象站应用,这个应用有两个公告板,可以分别显示目前的天气状况,当 WeatherObject 对象获得最新的测量数据时,公告板必须随时更新,而且这是一个可以扩展的气象站,气象站希望可以公布一组 API,让其他开发人员可以写出自己的公告板,可以插入此应用中,模型图如下:
WeatherData 知道如何和气象站取得联系,来获得天气的相关数据,当天气数据发生变化时,WeatherData 会更新相应的公告牌实时展示天气数据
1)先来看普通的实现方法
我们先来创建 WeatherData 类,实例代码如下
/**
* 从气象站获取的天气数据的 WeatherData 类
* 具有getter方法可以获取到温度、湿度、气压的测量值
* Created by qiudengjiao on 2017/6/5.
*/
public class WeatherData {
private float mTemperatrue;
private float mHumidity;
private float mPressure;
private CurrentConditionsDisplay mCurrentConditionsDisplay;
public WeatherData(CurrentConditionsDisplay currentConditionsDisplay) {
this.mCurrentConditionsDisplay = currentConditionsDisplay;
}
public float getTemperatrue() {
return mTemperatrue;
}
public float getHumidity() {
return mHumidity;
}
public float getPressure() {
return mPressure;
}
/**
* 模拟气象站更新数据
*
* @param mTemperatrue
* @param mHumidity
* @param mPressure
*/
public void setData(float mTemperatrue, float mHumidity, float mPressure) {
this.mTemperatrue = mTemperatrue;
this.mHumidity = mHumidity;
this.mPressure = mPressure;
dataChange();
}
/**
* 调用WeatherData的getXXX方法,以取得最新的测量值
* 通知公告板数据更新
*/
public void dataChange() {
mCurrentConditionsDisplay.updata(getTemperatrue(), getHumidity(), getPressure());
}
}
WeatherData 类主要是获取气象站传过来的数据,并通过 dataChange() 方法通知公告板进行实时的更新,实现的比较简单,相信大家一看就能明白
接下来看当前天气公告板 CurrentConditionsDisplay 类:
/**
* 当前天气展示公告板
* Created by qiudengjiao on 2017/6/5.
*/
public class CurrentConditionsDisplay {
private float mTemperatrue;
private float mHumidity;
private float mPressure;
/**
* 更新数据
*
* @param mTemperatrue
* @param mHumidity
* @param mPressure
*/
public void updata(float mTemperatrue, float mHumidity, float mPressure) {
this.mTemperatrue = mTemperatrue;
this.mHumidity = mHumidity;
this.mPressure = mPressure;
display();
}
/**
* 模拟公告板显示
*/
public void display() {
System.out.println("今天的温度:::" + mTemperatrue);
System.out.println("今天的湿度:::" + mHumidity);
System.out.println("今天的气压:::" + mPressure);
}
}
这里也非常简单,就是把从 WeatherData 类从气象站中获取的传递过来的数据进行展示,这里我们用 updata 来传递数据,用 display 模拟显示装置来展示数据
现在我们来测试我们写的层序是否可行:
/**
* 测试类
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData() {
CurrentConditionsDisplay currentConditionsDisply = new CurrentConditionsDisplay();
WeatherData weatherData = new WeatherData(currentConditionsDisply);
//模拟设置相应的参数
weatherData.setData(30, 50, 130);
}
}
打印效果如下:
这样我们就实现了相应的需求,但是现在我们来思考一下,我们的实现有什么不对?
这里我们的实现是针对具体的编程实现,试想一下,我们现在又有新的公告板,或者想删除刚才创建的公告板,这样我们就需要不停的去修改我们的程序,去增加或者删除公告板,不停地修改我们的程序显然不利于扩展,接下来我们就来看看着我们的项目中使用观察者模式是如何解决这些问题的
2)用观察者模式来实现
2.1 首先设计气象站:
先来看一下这个设计图,这张设计图其实是在我们前面模型图的扩展:
这里,Subject 抽象接口我们并不陌生,是我们的主题接口,现在 WeahterData 实现 Subject 接口,然后是观察者的抽象接口 Observer,所有气象组件(例如我们的公告板)都需要实现此观察者接口,这样主题在需要通知观察者时,有了一个共同的接口,然后我们也为公告板告板建立了一个共同的接口,公告板只需要实现 display() 方法
2.2 实现气象站
现在我们开始来实现这个系统了,Java 为观察者模式提供了内置支持,我们先暂时不用它,先自己动手,虽然有些时候可以利用 Java 内置支持,但是有些时候我们自己建立一个,这样会更具有弹性,那我们就从建立接口开始吧
首先建立抽象主题 Subject 接口:
/**
* 抽象主题接口
* Created by qiudengjiao on 2017/6/6.
*/
public interface Subject {
/**
* 注册观察者,需要一个观察者作为变量
*
* @param o
*/
public void registerObserver(Observer o);
/**
* 删除观察者,需要一个观察者作为变量
*
* @param o
*/
public void removeObserver(Observer o);
/**
* 当主题发生改变时,这个方法会被调用,以通知所有观察者
*/
public void notifyObservers();
}
接下来建立抽象观察者 Observer 接口:
/**
* 抽象观察者接口
* Created by qiudengjiao on 2017/6/6.
*/
public interface Observer {
/**
* 所有的观察者都必须实现update()方法,以实现观察者接口
* 当气象观察值发生改变时,主题会把以下状态值当做方法的参数,传送给观察者
*
* @param temperatrue
* @param humidity
* @param pressure
*/
public void update(float temperatrue, float humidity, float pressure);
}
然后我们再来建立公告板公共显示 DisplayElement 接口:
/**
* DisplayElement接口只包含了一个方法display
* Created by qiudengjiao on 2017/6/6.
*/
public interface DisplayElement {
/**
* 当公告板需要显示时调用此方法
*/
public void display();
}
由于我们在代码中已经做了详细的注解,就不再一一具体讲解
2.3 在 WeatherData 中实现 Subject 主题接口
/**
* WeathDate类
*/
public class WeatherDate implements Subject {
// 用ArraList来记录观察者
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherDate() {
observers = new ArrayList();
}
/**
* 注册观察者
*/
@Override
public void regiserObserver(Observer o) {
observers.add(o);
}
/**
* 删除观察者
*/
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}
/**
* 把状态通知每一个观察者,因为观察者都通知实现了update 所有我们知道如何通知它
*/
@Override
public void notifyObserver() {
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer) observers.get(i);
observer.update(temperature, humidity, pressure);
}
}
/**
* 当气象站得到更新观测值时,我们通知观察者
*/
public void dateChange() {
notifyObserver();
}
/**
* 模拟设置数据
*
* @param temperature
* @param humidity
* @param pressure
*/
public void setDate(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
dateChange();
}
}
2.4 现在我们来建立公告板
/**
* 此公告板实现了Observer接口,所以可以从WeatherDate对象中获得改变
* 它也实现了DisplayElement接口,因为我们规定,所用的公告板都行必须实现此接口
*
*/
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private Subject weatherDate;
private float temperature;
private float humidity;
private float pressure;
/**
* 构造器需要WeatherDate对象(也就是主题),作为注册之用
*
* @param weatherDate
*/
public CurrentConditionsDisplay(WeatherDate weatherDate) {
this.weatherDate = weatherDate;
weatherDate.regiserObserver(this);
}
/**
* 当update调用时,我们把相应的参数保存起来 然后调用display()
*/
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
/**
* display方法把今天的天气情况展示出来
*/
@Override
public void display() {
System.out.println("今天的温度是:::" + temperature);
System.out.println("今天的湿度是:::" + humidity);
System.out.println("今天的气压是:::" + pressure);
}
}
2.5 我们来测试一下,看看情况
/**
* 测试类
*/
public class WeatherStation {
public static void main(String[] args) {
// 首先,创建WeatherDate对象
WeatherDate weatherDate = new WeatherDate();
// 建立公告板
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherDate);
// 调用setDate,模拟设置气象数据
weatherDate.setDate(30, 40, 150);
}
}
2.6 测试台打印情况如下:
这样我们就成功实现了天气情况的展示,在使用观察者模式之后,和原来的实现方式相比就有了更好的扩展性,很好的解决了我们的问题,接下来我们再来创建一个新的公告板,看看扩展效果
2.7 建立一个新的公告板
/**
* 天气预报公告板
*/
public class ForecastDisplay implements Observer, DisplayElement {
private Subject weatherDate;
private float temperature;
private float humidity;
private float pressure;
/**
* 构造器需要WeatherDate对象(也就是主题),作为注册之用
*
* @param weatherDate
*/
public ForecastDisplay(WeatherDate weatherDate) {
this.weatherDate = weatherDate;
weatherDate.regiserObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
@Override
public void display() {
System.out.println("明天的温度是:::" + temperature);
System.out.println("明天的湿度是:::" + humidity);
System.out.println("明天的气压是:::" + pressure);
}
}
2.8 现在我们来修改一下测试类,代码如下:
/**
* 测试类
*/
public class WeatherStation {
public static void main(String[] args) {
// 首先,创建WeatherDate对象
WeatherDate weatherDate = new WeatherDate();
// 建立公告板
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherDate);
// 建立预测公告板
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherDate);
// 调用setDate,模拟设置气象数据
weatherDate.setDate(30, 40, 150);
}
}
这里我们添加了新的公告板,下面我们来看看打印的结果:
这样新加入的公告板就实现了,解决了修改代码的问题,使得程序更加的易于扩展,也更加的健壮,大家可以自行模拟删除公告板,也是非常的简单,这里我们就不再演示,到目前为止,我们已经从无到有的完成了观察者模式,但是 Java API 有内置的观察者模式,java.util(package)内包含最基本的 Observer 接口与 Observable 类,这和我们的 Subject 接口与 Observer 接口很相似, Observer 接口与 Observable 类使用上更加方便,因为许多功能都已经实现好了,接下来我们就来看使用 Java 内置的观察者模式如何来实现我们刚才的需求
六、使用 Java 内置的观察者模式
首先,把 WeatherDate 改成使用 java.util.Observable
import java.util.Observable;
/**
* 从气象站获取的天气数据的 WeatherData 类
* 现在继承内置的 Observable
* 我们不需要追踪观察者了,也不需要管理注册与删除(超类代替即可)
* 所以我们把注册和删除的代码删掉
* Created by qiudengjiao on 2017/6/5.
*/
public class WeatherData extends Observable {
private float mTemperatrue;
private float mHumidity;
private float mPressure;
/**
* 构造函数也不用为了记住观察者们而创建数据集合了
*/
public WeatherData() {
}
/**
* 当气象站得到跟新观测值时,通知观察者
*/
public void dataChange() {
//在调用notifyObservers()之前,必须要先调用setChanged()来指示状态已经改变
setChanged();
notifyObservers();
}
/**
* 模拟气象站设置更新数据
*
* @param temperatrue
* @param humidity
* @param pressure
*/
public void setData(float temperatrue, float humidity, float pressure) {
this.mTemperatrue = temperatrue;
this.mHumidity = humidity;
this.mPressure = pressure;
dataChange();
}
public float getTemperatrue() {
return mTemperatrue;
}
public float getHumidity() {
return mHumidity;
}
public float getPressure() {
return mPressure;
}
}
接下来我们重做 CurrentConditionsDisplay 公告板:
import java.util.Observable;
import java.util.Observer;
/**
* 当前天气展示公告板
* 实现Java内置的Observer接口
* Created by qiudengjiao on 2017/6/5.
*/
public class CurrentConditionsDisplay implements Observer {
private Observable observable;
private float mTemperatrue;
private float mHumidity;
private float mPressure;
/**
* 现在构造函数把Observable当参数
* 并将CurrentConditionsDisplay登机为观察者
*
* @param observable
*/
public CurrentConditionsDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
/**
* 改变update()方法,增加Observable和数据对象作为参数
*
* @param o
* @param arg
*/
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherData) {
WeatherData weatherData = (WeatherData) o;
this.mTemperatrue = weatherData.getTemperatrue();
this.mHumidity = weatherData.getHumidity();
this.mPressure = weatherData.getPressure();
display();
}
}
/**
* 模拟公告板显示
*/
public void display() {
System.out.println("今天的温度:::" + mTemperatrue);
System.out.println("今天的湿度:::" + mHumidity);
System.out.println("今天的气压:::" + mPressure);
}
}
测试类:
/**
* 测试类
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData() {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentConditionsDisply = new CurrentConditionsDisplay(weatherData);
//模拟设置相应的参数
weatherData.setData(30, 50, 130);
}
}
我们来看看控制台打印结果如下:
这样我们就在使用 Java 内置类的情况下也实现了业务需求,接下来我们来分析下使用 Java 内置方法有一点的不好处,这里大家注意到Observable 是一个类,你必须设计一个类去继承它,如果某类想同时具有 Observable 和另一个超类时,就会陷入两难,毕竟 Java 不支持多继承,这限制了 Observable 的复用潜力(而增加复用潜力正是我们设计模式的最原始动机),所以大家在使用观察者模式时也要相应的注意,好了,观察者模式我们到这里就讨论完毕了