编程思想之消息机制

21CTO导读:本文详细描述了编程中的消息机制,包括Windows和Java体系。



0_EYkQh1w1msyxqcl6.png


1.编程思想之消息机制什么是消息?

何为消息?消息就是带有某种信息的信号,如你用鼠标点击一个窗口会产生鼠标的消息,键盘输入字符会产生键盘的消息,一个窗口大小的改变也会产生消息。 

消息从何而来?根据冯·诺依曼的体系结构计算机有运算器、存储器、控制器和输入设备和输出设备五大部件组成,消息主要来自输入设备,如键盘、鼠标、扫描仪等,也可来自已窗口和操作系统。 

消息机制的三大要点:消息队列、消息循环(分发)、消息处理。其结构如下: 

/uploads/fox/01171634_0.jpg
图 1 :消息机制原理

消息队列就是存放消息的一种队列,具有先进先出的特点。每产生一个消息都会添加进消息队列中,在Window中消息队列是在操作系统中定义的。消息队列就如同一群排队打饭的少男少女,这群人中光景较好的排在前面,光景较差的排在后面,可以理解成是一种优先级队列!要想更多的了解队列的相关知识,可参见队列。 

消息循环就是通过循环(如while)不断地从消息队列中取得队首的消息,并将消息分发出去。类似于上面的例子中分发饭菜值日生。 

消息处理就是在接收到消息之后根据不同的消息类型做出不同的处理。上面例子中值日生根据学生不同类型的饭票给他们不同等级的饭菜就是消息处理,学生手中的饭票就是消息所携带的信息。 

事件是根据接收到的消息的具体信息做出的特定的处理,放在代码中是事件响应函数。上面的例子中学生拿到饭菜后吃饭就是具体的事件。
 
消息机制模拟
 
在这里我们以控制台输入信息模拟窗口、对话框接收鼠标、键盘等消息,以ArrayBlockingQueue对象存放消息队列。在控制台中输入一个数值和一个字符串代表一个消息,输入-1结束输入。
 
模拟代码如下:
package message;

import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;

/**
* 消息
* @author luoweifu
*/
class Message {
//消息类型
public static final int KEY_MSG = 1;
public static final int MOUSE_MSG = 2;
public static final int SYS_MSG = 3;

private Object source; //来源
private int type; //类型
private String info; //信息

public Message(Object source, int type, String info) {
super();
this.source = source;
this.type = type;
this.info = info;
}

public Object getSource() {
return source;
}
public void setSource(Object source) {
this.source = source;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
public static int getKeyMsg() {
return KEY_MSG;
}
public static int getMouseMsg() {
return MOUSE_MSG;
}
public static int getSysMsg() {
return SYS_MSG;
}
}


interface MessageProcess {
public void doMessage(Message msg);
}

/**
* 窗口模拟类
*/
class WindowSimulator implements MessageProcess{
private ArrayBlockingQueue msgQueue;
public WindowSimulator(ArrayBlockingQueue msgQueue) {
this.msgQueue = msgQueue;
}

public void GenerateMsg() {
while(true) {
Scanner scanner = new Scanner(System.in);
int msgType = scanner.nextInt();
if(msgType < 0) { //输入负数结束循环
break;
}
String msgInfo = scanner.next();
Message msg = new Message(this, msgType, msgInfo);
try {
msgQueue.put(msg); //新消息加入到队尾
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

@Override
/**
* 消息处理
*/
public void doMessage(Message msg) {
switch(msg.getType()) {
case Message.KEY_MSG:
onKeyDown(msg);
break;
case Message.MOUSE_MSG:
onMouseDown(msg);
break;
default:
onSysEvent(msg);
}
}

//键盘事件
public static void onKeyDown(Message msg) {
System.out.println("键盘事件:");
System.out.println("type:" + msg.getType());
System.out.println("info:" + msg.getInfo());
}

//鼠标事件
public static void onMouseDown(Message msg) {
System.out.println("鼠标事件:");
System.out.println("type:" + msg.getType());
System.out.println("info:" + msg.getInfo());
}

//操作系统产生的消息
public static void onSysEvent(Message msg) {
System.out.println("系统事件:");
System.out.println("type:" + msg.getType());
System.out.println("info:" + msg.getInfo());
}
}

/**
* 消息模拟
* @author luoweifu
*/
public class MessageSimulator {
//消息队列
private static ArrayBlockingQueue<Message> messageQueue = new ArrayBlockingQueue<Message>(100);

public static void main(String args) {
WindowSimulator generator = new WindowSimulator(messageQueue);
//产生消息
generator.GenerateMsg();

//消息循环
Message msg = null;
while((msg = messageQueue.poll()) != null) {
((MessageProcess) msg.getSource()).doMessage(msg);
}
}
}

这里模拟用例中只有一个消息输入源,且是一种线程阻塞的,只有输入结束后才会进行消息的处理。
 
真实的Windows操作系统中的消息机制会有多个消息输入源,且消息输入的同时也能进行消息的处理。
 
2.C++中的消息机制从简单例子探析核心原理

在讲之前,我们先看一个简单例子:创建一个窗口和两个按钮,用来控制窗口的背景颜色。其效果如下:
 
/uploads/fox/01171634_1.jpg /uploads/fox/01171634_2.jpg 
图 2 :效果图

Win32Test.h之代码如下:
#pragma once

#include <windows.h>
#include <atltypes.h>
#include <tchar.h>

//资源ID
#define ID_BUTTON_DRAW 1000
#define ID_BUTTON_SWEEP 1001

// 注册窗口类
ATOM AppRegisterClass(HINSTANCE hInstance);
// 初始化窗口
BOOL InitInstance(HINSTANCE, int);
// 消息处理函数(又叫窗口过程)
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// (白色背景)按钮事件
void OnButtonWhite();
// (灰色背景)按钮事件
void OnButtonGray();
// 绘制事件
void OnDraw(HDC hdc);
Win32Test.cpp 代码如下:
#include "stdafx.h"
#include "Win32Test.h"


//字符数组长度
#define MAX_LOADSTRING 100

//全局变量
HINSTANCE hInst; // 当前实例
TCHAR g_szTitle[MAX_LOADSTRING] = TEXT("Message process"); // 窗口标题
TCHAR g_szWindowClass[MAX_LOADSTRING] = TEXT("AppTest"); // 窗口类的名称
HWND g_hWnd; // 窗口句柄
bool g_bWhite = false; // 是否为白色背景

//WinMain入口函数
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// 注册窗口类
if(!AppRegisterClass(hInstance))
{
return (FALSE);
}
// 初始化应用程序窗口
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}

// 消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int) msg.wParam;
}



// 注册窗口类
ATOM AppRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = g_szWindowClass;
wcex.hIconSm = NULL;

return RegisterClassEx(&wcex);
}



// 保存实例化句柄并创建主窗口
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 保存handle到全局变量
g_hWnd = CreateWindow(g_szWindowClass, g_szTitle, WS_OVERLAPPEDWINDOW, 0, 0, 400, 300, NULL, NULL, hInstance, NULL);
// 创建按钮
HWND hBtWhite = CreateWindowEx(0, L"Button", L"白色", WS_CHILD | WS_VISIBLE | BS_TEXT, 100, 100, 50, 20, g_hWnd, (HMENU)ID_BUTTON_DRAW, hInst, NULL);
HWND hBtGray = CreateWindowEx(0, L"Button", L"灰色", WS_CHILD | WS_VISIBLE | BS_CENTER, 250, 100, 50, 20, g_hWnd, (HMENU)ID_BUTTON_SWEEP, hInst, NULL);

if (!g_hWnd)
{
return FALSE;
}
ShowWindow(g_hWnd, nCmdShow);
UpdateWindow(g_hWnd);

return TRUE;
}



// (窗口)消息处理
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;

switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
//wmEvent = HIWORD(wParam);

switch (wmId)
{
case ID_BUTTON_DRAW:
OnButtonWhite();
break;
case ID_BUTTON_SWEEP:
OnButtonGray();
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
OnDraw(hdc);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}



//事件处理

//按下hBtWhite时的事件
void OnButtonWhite()
{
g_bWhite = true;
InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口
}

//按下hBtGray时的事件
void OnButtonGray()
{
g_bWhite = false;
InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口
}

//绘制事件(每次刷新时重新绘制图像)
void OnDraw(HDC hdc)
{
POINT oldPoint;
SetViewportOrgEx(hdc, 0, 0, &oldPoint);
RECT rcView;
GetWindowRect(g_hWnd, &rcView); // 获得句柄的画布大小
HBRUSH hbrWhite = (HBRUSH)GetStockObject(WHITE_BRUSH);
HBRUSH hbrGray = (HBRUSH)GetStockObject(GRAY_BRUSH);
if (g_bWhite)
{
FillRect(hdc, &rcView, hbrWhite);
} else
{
FillRect(hdc, &rcView, hbrGray);
}
SetViewportOrgEx(hdc, oldPoint.x, oldPoint.y, NULL);
}

在上面这个例子中,消息的流经过程如下: 

/uploads/fox/01171634_3.jpg
 
图 3 :消息的流经过程

这与《编程思想之消息机制》中图1(消息机制原理)是相吻合的,这就是Windows消息机制的核心部分,也是Windows API开发的核心部分。Windows系统和Windows下的程序都是以消息为基础,以事件为驱动。 

RegisterClassEx的作用是注册一个窗口,在调用CreateWindow创建一个窗口前必须向windows系统注册获惟一的标识。
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

这个while循环就是消息循环,不断地从消息队列中获取消息,并通过DispatchMessage(&msg)将消息分发出去。消息队列是在Windows操作系统中定义的(我们无法看到对应定义的代码),对于每一个正在执行的Windows应用程序,系统为其建立一个“消息队列”,即应用程序队列,用来存放该程序可能创建的各种窗口的消息。DispatchMessage会将消息传给窗口函数(即消息处理函数)去处理,也就是WndProc函数。WndProc是一个回调函数,在注册窗口时通过wcex.lpfnWndProc将其传给了操作系统,所以DispatchMessage分发消息后,操作系统会调用窗口函数(WndProc)去处理消息。关于回调函数可参考:回调函数。 

每一个窗口都应该有一个函数负责消息处理,程序员必须负责设计这个所谓的窗口函数WndProc。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 中的四个参数就是消息的相关信息(消息来自的句柄、消息类型等),函数中通过switch/case根据不同的消息类型分别进行不同的处理。在收到相应类型的消息之后,可调用相应的函数去处理,如OnButtonWhite、OnButtonGray、OnDraw,这就是事件处理的雏形。
 
在default中调用了DefWindowProc,DefWindowProc是操作系统定义的默认消息处理函数,这是因为所有的消息都必须被处理,应用程序不处理的消息需要交给操作系统处理。
 
消息的定义和类型
 
Windows消息都以WM_为前缀,意思是”Windows Message”,如WM_CREATE、WM_PAINT等。消息的定义如下:
typedef struct tagMsg
{
HWND hwnd; //接受该消息的窗口句柄
UINT message; //消息常量标识符,也就是我们通常所说的消息号
WPARAM wParam; //32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; //32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; //消息创建时的时间
POINT pt; //消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;
消息主要有三种类型: 

1. 命令消息(WM_COMMAND):命令消息是程序员需要程序做某些操作的命令。凡UI对象产生的消息都是这种命令消息,可能来自菜单、加速键或工具栏按钮等,都以WM_COMMAND呈现。 

2. 标准窗口消息:除WM_COMMAND之处,任何以WM_开头的消息都是这一类。标准窗口消息是系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都会激发窗口消息,以及鼠标移动、点击,键盘输入都是属于这种消息。 

3. Notification:这种消息由控件产生,为的是向其父窗口(通常是对话框窗口)通知某种情况。当一个窗口内的子控件发生了一些事情,而这些是需要通知父窗口的,此刻它就上场啦。通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。
 
队列消息和非队列消息
 
Windows中有一个系统消息队列,对于每一个正在执行的Windows应用程序,系统为其建立一个“消息队列”,即应用程序队列,用来存放该程序可能创建的各种窗口的消息。 

(1)队列消息(Queued Messages) 
消息会先保存在消息队列中,通过消息循环从消息队列中获取消息并分发到各窗口函数去处理,如鼠标、键盘消息就属于这类消息。 

(2)非队列消息(NonQueued Messages) 
就是消息会直接发送到窗口函数处理,而不经过消息队列。 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED就属于此类。
 
PostMessage与SendMessage的区别
 
PostMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理,等消息被处理后才返回。 

/uploads/fox/01171634_4.jpg 
图 4 :消息队列示意图

为证明这一过程,我们可以改动一下上面的这个例子。 

1.在Win32Test.h中添加ID_BUTTON_TEST的定义
#define ID_BUTTON_TEST      1002

2.在OnButtonWhite中分别用SendMessage和PostMessage发送消息 
//按下hBtWhite时的事件
void OnButtonWhite()
{
g_bWhite = true;
InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口
SendMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0);
//PostMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0);
}
3.在消息循环中增加ID_BUTTON_TEST的判断
while (GetMessage(&msg, NULL, 0, 0))
{
if (LOWORD(msg.wParam) == ID_BUTTON_TEST)
{
OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint1]
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}


4.在窗口处理函数WndProc增加ID_BUTTON_TEST的判断
case ID_BUTTON_TEST:
{
OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint2]
}
break;
case ID_BUTTON_DRAW:
OnButtonWhite();
break;
case ID_BUTTON_SWEEP:
OnButtonGray();
break;

用断点调试的方式我们发现,用SendMessage发送的ID_BUTTON_TEST消息只会进入BreakPoint2,而PostMessage发送的ID_BUTTON_TEST会进入到BreakPoint1和BreakPoint2。
 
3.Java中的消息机制与观察者模式从简单的例子开始

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

/uploads/fox/01171634_5.jpg 
图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();
}
}
上面这个例子中,窗口和所有的控件创建完成之后,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();
}
}
结果如下:

水开了,关电源…


这个代码非常简单,水开了就会通知人,人就去关电源。但也有一个问题,就是拓展性不好,不灵活。如果我们烧的开水不是用来喝,而用来洗澡,我就要监测它的温度,可能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);
}
}
结果如下:

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


 
观察者模式设计:

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

观察者模式的类结构关系如下: 
/uploads/fox/01171919_6.jpg 
观察者模式的类图结构

在设计观察者模式的程序时要注意以下几点: 

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();
}
}
[/i]

Observer接口:
[i]package java.util;

public interface Observer {
void update(Observable o, Object arg);
}
[/i]

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

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

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

作者:luoweifu
来源:21CTO.COM


0 个评论

要回复文章请先登录注册