java:从消息机制谈到观察者模式

时间:2021-02-17 22:45:02

从简单的例子开始

同样,我们还是先看一个简单例子:创建一个窗口实现加法的计算功能。其效果如下:

java:从消息机制谈到观察者模式
图1: 加法计算

Calculator.java:

import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; /**
* Created with IntelliJ IDEA.
* User: luoweifu
* Date: 15-5-5
* Time: 下午9:14
* To change this template use File | Settings | File Templates.
*/
public class Calculator {
/**
* 主窗口的宽度
*/
public static final int WIDTH = 500;
/**
* 主窗口的高度
*/
public static final int HEIGHT = 100; private JFrame frameCalculator;
private JEditorPane editAddend1;
private JEditorPane editAddend2;
private JEditorPane editResult;
private JLabel labelPlus;
private JButton btEqual;
public Calculator() {
frameCalculator = new JFrame();
} public void launchFrame() {
frameCalculator.setSize(WIDTH, HEIGHT);
frameCalculator.setLocationRelativeTo(null);
frameCalculator.setTitle("加法计算"); Container container = frameCalculator.getContentPane();
container.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
editAddend1 = new JEditorPane();
editAddend1.setBorder(new BevelBorder(BevelBorder.LOWERED));
editAddend2 = new JEditorPane();
editAddend2.setBorder(new BevelBorder(BevelBorder.LOWERED));
labelPlus = new JLabel("+");
btEqual = new JButton("=");
editResult = new JEditorPane();
editResult.setBorder(new BevelBorder(BevelBorder.LOWERED));
editResult.setEditable(false);
container.add(editAddend1);
container.add(labelPlus);
container.add(editAddend2);
container.add(btEqual);
container.add(editResult);
frameCalculator.setVisible(true);
//frameCalculator.setDefaultCloseOperation(EXIT_ON_CLOSE); class AdditionCalculate implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
int add1 = Integer.parseInt(editAddend1.getText());
int add2 = Integer.parseInt(editAddend2.getText());
int result = add1 + add2;
editResult.setText(result + "");
}
} AdditionCalculate additionCalculate = new AdditionCalculate();
btEqual.addActionListener(additionCalculate);
} public static void main(String args[]) {
Calculator calculator = new Calculator();
calculator.launchFrame();
}
}
  • 1

上面这个例子中,窗口和所有的控件创建完成之后,btEqual按钮邦定了一个监听对象additionCalculate,一旦这个按钮被点击,就会通知additionCalculate对象,additionCalculate对象监听到点击事件,就会调用actionPerformed方法作出相应的响应。additionCalculate是内部类AdditionCalculate的对象,AdditionCalculate实现了ActionListener 接口。
通过上面的例子,你也许看出来了Java Swing/AWT包中窗口、控件的响应方式是一种源-监听器(Source/Listener)模式,也叫做观察者模式,这种机制常称为事件机制。

事件机制与消息机制的区别

Windows API可以开发窗口(界面)程序,Java通过Swing/AWT包也可以开发窗口(界面)程序,那么他们之间有什么异同呢?
1. 实现方式不同,Windows API主要是通过回调,提供对外的接口由用户去实现对应的处理,内部由操作系统实现,我们看不到;Java中的Swing/AWT主要源-监听器(观察者)模式,实现窗口(控件)对象与事件处理对象的邦定。
2. Windows API的消息机制有一个消息循环一直在接收消息,它是线程阻塞的。而Java的的Swing/AWT是一个通知方式,只有窗口(控件)有变化(被鼠标、键盘等触发)时才会通知监听者去处理,是非阻塞的。
3. 相同点:都有消息源——窗口(控件),都有消息处理,Windows API是窗口处理函数,Java中是监听者的处理方法,都有消息(Java叫事件Event)。如果把Windows API中消息队列和消息循环去掉,两者就很像了,就如同使用SendMessage直接把消息发送到窗口处理函数。所以,事件机制也可以认为是特殊的消息机制。

既然Java中的窗口程序是通过源-监听器(观察者)模式实现的,我们就有必要讨论一下观察者模式了。


观察者模式

观察者模式,顾名思意就是观察与被观察的关系,比如你在烧开水得时时看着它开没开,你就是观察者,开水就是被观察者;再比如说你在带小孩,你关注她是不是饿了,是不是喝了,是不是撒尿了,你就是观察者,小孩就被观察者。观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。当你看这些模式的时候,不要觉得陌生,它们就是观察者模式。

观察者模式一般是一种一对多的关系,可以有任意个(一个或多个)观察者对象同时监听某一个对象。监听的对象叫观察者(后面提到监听者,其实就指观察者,两者是等价的),被监听的对象叫被观察者(Observable,也叫主题Subject)。被观察者对象在状态上发生变化时,会通知所有观察者对象,使它们能够做出相应的变化(如自动更新自己的信息)。
我们就以上面提到的烧开水的一个简单生活实例来模拟一下观察者模式。
代码ObserverModule.java:

//人,观察者
class Person {
public void update(String data) {
System.out.println(data + "关电源...");
}
} //水,被观察者
class Water {
private Person person;
private boolean isBoiled;
public Water() {
isBoiled = false;
} public void SetBoiled() {
isBoiled = true;
notifyObserve();
}
public void addObserver(Person person) {
this.person = person;
} public void removeObserver() {
if (person != null) {
person = null;
}
} public void notifyObserve() {
if (isBoiled && person != null) {
person.update("水开了,");
isBoiled = false;
}
}
} //客户端
public class ObserverModule {
public static void main(String args[]) {
Person person = new Person();
Water water = new Water();
water.addObserver(person);
water.SetBoiled();
}
}
  • 1

结果如下:

水开了,关电源…

这个代码非常简单,水开了就会通知人,人就去关电源。但也有一个问题,就是拓展性不好,不灵活。如果我们烧的开水不是用来喝,而用来洗澡,我就要监测它的温度,可能50度就关电源,也可能要60度才行,这样一个监听就不够了,还监听温度的随时变化;再比如水开了之后,我不是关电源,而是让它保温。你的updae又得改了……
所以上面这个代码拓展行是不好,但已经实现了我们的基本想法,我们算是我们的第一个版本(版本)。接下来我们再看一下,升级版:
版本2:ObserverModule.java

//观察者
interface Observer {
public void update(Observable observable);
} //被观察者
abstract class Observable {
protected boolean isChanaged;
protected List<Observer> observers = new ArrayList<Observer>(); public Observable() {
isChanaged = false;
}
public void addObserver(Observer observer) {
observers.add(observer);
} public void removeObserver(Observer observer) {
observers.remove(observer);
} public void removeObservers() {
observers.clear();
}
public void notifyObservers() {
if (isChanaged) {
for (int i = 0; i < observers.size(); i ++) {
observers.get(i).update(this);
}
isChanaged = false;
}
}
} //人,温度监测
class TemperatureObserver implements Observer{
@Override
public void update(Observable observable) {
Water water = (Water)observable;
System.out.println("温度:" + water.getTemperature() + " 状态:" + water.getStatus());
System.out.println("TemperatureObserver observing...");
}
} class BoildObserver implements Observer {
String doSomthing;
BoildObserver(String doSomthing) {
this.doSomthing = doSomthing;
} @Override
public void update(Observable observable) {
Water water = (Water)observable;
if (water.getTemperature() >= 100) {
System.out.println("状态:" + water.getStatus());
System.out.println("BoildObserver:" + doSomthing);
} }
}
//水,被观察者
class Water extends Observable{
private double temperature;
private String status; public Water() {
super();
this.temperature = 0;
this.status = "冷水";
} public Water(Observer observer) {
this();
observers.add(observer);
} public double getTemperature() {
return temperature;
} public String getStatus() {
return status;
} public void change(double temperature) {
this.temperature = temperature;
if (temperature < 40) {
status = "冷水";
} else if (temperature >= 40 && temperature < 60) {
status = "温水";
}else if (temperature >= 60 && temperature < 100 ) {
status = "热水";
} else {
status = "开水";
}
this.isChanaged = true;
notifyObservers();
}
} //客户端
public class ObserverModule {
public static void main(String args[]) {
TemperatureObserver temperatureObserver = new TemperatureObserver();
BoildObserver boildObserver1 = new BoildObserver("关闭电源...");
BoildObserver boildObserver2 = new BoildObserver("继续保湿...");
Water water = new Water(temperatureObserver);
water.addObserver(boildObserver1);
water.addObserver(boildObserver2);
water.change(45);
water.change(80);
water.change(100);
}
}
  • 1

结果如下:

温度:45.0 状态:温水
TemperatureObserver observing…
温度:80.0 状态:热水
TemperatureObserver observing…
温度:100.0 状态:开水
TemperatureObserver observing…
状态:开水
BoildObserver:关闭电源…
状态:开水
BoildObserver:继续保湿…

观察者模式设计:

通过上面这个活生生的例子,我们总结一下观察者模式的设计。
观察者模式的类结构关系如下:

java:从消息机制谈到观察者模式
观察者模式的类图结构

在设计观察者模式的程序时要注意以下几点:
1. 要明确谁是观察者谁是被观察者,只要明白谁是关注对象,问题也就明白了。一般观察者与被观察者之间的是多对一的关系,一个被观察对象可以有多个监听对象(观察者)。如一个编辑框,有鼠标点击的监听者,也有键盘的监听者,还有内容改变的监听者。
2. Observable在发送广播通知的时候,无须指定具体的Observer,Observer可以自己决定是否要订阅Subject的通知。
3. 被观察者至少需要有三个方法:添加监听者、移除监听者、通知Observer的方法;观察者至少要有一个方法:更新方法,更新当前的内容,作出相应的处理。
注:添加监听者、移除监听者在不同的模型中可能会有不同命名,如观察者模型中一般,addObserver、removeObserver;在源-监听器(Source/Listener)模型中一般是attach/detach,应用在桌面编程的窗口中,还可能是attachWindow/detachWindow,或Register/UnRegister。不要被名称迷糊了,不管他们是什么名称,其实功能都是一样的,就是添加/删除观察者。
4. 观察者模式的应用场景: <1>.对一个对象状态的更新需要其他对象同步更新;,或者一个对象的更新需要依赖另一个对象的更新;<2>.对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节,如消息推送。

推模型和拉模型

观察者模式根据其侧重的功能还可以分为推模型和拉模式
推模型:被观察者对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。一般这种模型的实现中,会把被观察者对象中的全部或部分信息通过update的参数传递给观察者[update(Object obj) ]。
拉模型:被观察者在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到被观察者对象中获取,相当于是观察者从被观察者对象中拉数据。一般这种模型的实现中,会把被观察者对象自身通过update方法传递给观察者[update(Observable observable ) ],这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

JDK对观察者模式的支持

其实JDK已经提供了对观察者模式接口的定义了。在java.util库里面,提供了一个Observable类以及一个Observer接口,构成JAVA语言对观察者模式的支持。我们可以看一下Java中的源码:
Observable接口:

package java.util;

public class Observable {
private boolean changed = false;
private Vector obs; public Observable() {
obs = new Vector();
} public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
} public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
} public void notifyObservers() {
notifyObservers(null);
} public void notifyObservers(Object arg) { Object[] arrLocal; synchronized (this) { if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
} for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
} public synchronized void deleteObservers() {
obs.removeAllElements();
} protected synchronized void setChanged() {
changed = true;
} protected synchronized void clearChanged() {
changed = false;
} public synchronized boolean hasChanged() {
return changed;
} public synchronized int countObservers() {
return obs.size();
}
}
  • 1

Observer接口:

package java.util;

public interface Observer {
void update(Observable o, Object arg);
}
  • 1

通过前面的分析,再来看Java的源码,相信不会太难了。这里有个比较好的地方是Observable类中的addObserver、deleteObserver、notifyObservers等方法已经帮我们考虑了线程同步的问题,这样更安全。


回归本质

我们再回顾一下加法计算器的例子。通过观察者模式的分析,也许你已经清楚了,AdditionCalculate的对象additionCalculate就是观察者;JButton的对象btEqual就是被观察者,同时也是消息源;btEqual.addActionListener(additionCalculate);就是添加监听者。ActionListener中的public void actionPerformed(ActionEvent e)就相当于update方法,只不过参数e消息源产生的消息(事件)。

观察者模式还可以用于网络中的客户端和服务器,比如手机中的各种App的消息推送,服务端是观察者,各个手机App是被观察者,一旦服务器上的数据(如App升级信息)有更新,就会被推送到手机客户端。