程式設計思想之訊息機制
1.程式設計思想之訊息機制什麼是訊息?
何為訊息?訊息就是帶有某種資訊的訊號,如你用滑鼠點選一個視窗會產生滑鼠的訊息,鍵盤輸入字元會產生鍵盤的訊息,一個視窗大小的改變也會產生訊息。
訊息從何而來?根據馮·諾依曼的體系結構計算機有運算器、儲存器、控制器和輸入裝置和輸出裝置五大部件組成,訊息主要來自輸入裝置,如鍵盤、滑鼠、掃描器等,也可來自已視窗和 ofollow,noindex" target="_blank">作業系統 。
訊息機制的三大要點:訊息佇列、訊息迴圈(分發)、訊息處理。其結構如下:
圖 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++中的訊息機制從簡單例子探析核心原理
在講之前,我們先看一個簡單例子:建立一個視窗和兩個按鈕,用來控制視窗的背景顏色。其效果如下:
圖 2 :效果圖
Win32Test.h之程式碼如下:
#pragma once #include <windows.h> #include <atltypes.h> #include <tchar.h> //資源ID #define ID_BUTTON_DRAW1000 #define ID_BUTTON_SWEEP1001 // 註冊視窗類 ATOM AppRegisterClass(HINSTANCE hInstance); // 初始化視窗 BOOL InitInstance(HINSTANCE, int); // 訊息處理函式(又叫視窗過程) LRESULT CALLBACKWndProc(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); }
在上面這個例子中,訊息的流經過程如下:
圖 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 { HWNDhwnd;//接受該訊息的視窗控制代碼 UINTmessage;//訊息常量識別符號,也就是我們通常所說的訊息號 WPARAMwParam;//32位訊息的特定附加資訊,確切含義依賴於訊息值 LPARAMlParam;//32位訊息的特定附加資訊,確切含義依賴於訊息值 DWORDtime;//訊息建立時的時間 POINTpt;//訊息建立時的滑鼠/游標在螢幕座標系中的位置 }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傳送的訊息是非佇列訊息, 被直接送到視窗過程處理,等訊息被處理後才返回。
圖 4 :訊息佇列示意圖
為證明這一過程,我們可以改動一下上面的這個例子。
1.在Win32Test.h中新增ID_BUTTON_TEST的定義
#define ID_BUTTON_TEST1002
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中的訊息機制與觀察者模式從簡單的例子開始
同樣,我們還是先看一個簡單例子:建立一個視窗實現加法的計算功能。其效果如下:
圖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 classObservable { 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:繼續保溼…
觀察者模式設計:
通過上面這個活生生的例子,我們總結一下觀察者模式的設計。
觀察者模式的類結構關係如下:
觀察者模式的類圖結構
在設計觀察者模式的程式時要注意以下幾點:
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