1.問題的提出
以前做一個界面的時候常常會遇到這樣的尴尬情況:希望保留各個獨立的組件(類),但又希望它們之間能夠相互通信。譬如Windows中的Explorer,我們希望鼠標點擊左邊是樹型目錄的一個節點,右邊的文件浏覽能及時列出該節點目錄下的文件和子目錄,類似這樣一個簡單的應用,如果只有一個類繼承JFrame,而樹型組件和浏覽文件的面板作為成員,就像:
public class MainFrame extends JFrame
{
JPanel treePanel;
JTree tree;
JPanel filePanel;
...
}
這樣當然容易在兩者之間傳遞消息,但是可擴展性較差。通常容易想到的是兩種辦法:在一個組件裡保留另一個組件類型的成員,初始化時作為參數傳入引用,比如:
class TreePanel extends JPanel
{
JTree tree;
...
}
class FilePanel extends JPanel
{
public FilePanel(JTree tree){...}
...
}
或者將一個組件線程化,不停地監聽另一個組件的變化,然後作出相應的反映,比如:
class TreePanel extends JPanel
{
JTree tree;
...
}
class FilePanel extends JPanel implements Runnable
{
public void run()
{
while (true)
{
//監聽tree的變化
}
...
}
...
}
這樣確實可以達到我們的目的,但是第一種方案顯然不利於松散耦合,第二種方案比較占用系統資源。通過學習設計模式,我們發現可以用Observer模式來解決這個問題。
2.Observer模式
設計模式分為創建型、結構型和行為型,其中行為型模式專門處理對象間通信,指定交互方式等,Observer模式就是屬於行為型的一種設計模式。按照“四人幫”(Gang of Four)在“Design Patterns”裡的定義,Observer模式“定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時, 所有依賴於它的對象都得到通知並被自動更新”,這個描述正好符合我們對“組件通信”問題的需求。讓我們先看看Observer模式的結構:
其中各元素的含義如下:
Subject:被觀察的目標的抽象接口,它提供對觀察者(Observer)的注冊、注銷服務,Notify方法通知Observer目標發生改變;
Object:觀察者的抽象接口,Update方法是當得到Subject狀態變化的通知後所要采取的動作;
ConcreteSubject:Subject的具體實現;
ConcreteObserver:Observer的具體實現
Observer模式在實現MVC結構時非常有用,為數據和數據表示解耦合。
3.Java中的Observer模式:Observer和Observable
在大致了解了Observer模式的描述之後,現在我們更為關心的是它在Java中是如何應用的。幸運的是,自從JDK 1.0起,就有了專門處理這種應用的API,這就是Observer接口和Observable類,它們是屬於java.util包的一部分。看來Java的開發者們真是深谙設計模式的精髓,而Java的確是為了真正的面向對象而生的,呵呵!
這裡的Observer和Observable分別對應設計模式中的Observer和Subject,對比一下它們定義的方法,痕跡還是相當明顯的:
Observer的方法:
update(Observable subject, Object arg) 監控subject,當subject對象狀態發生變化時Observer會有什麼響應,arg是傳遞給Observable的notifyObservers方法的參數;
Observable的方法:
addObserver(Observer observer) observer向該subject注冊自己
hasChanged() 檢查該subject狀態是否發生變化
setChanged() 設置該subject的狀態為“已變化”
notifyObservers() 通知observer該subject狀態發生變化
4.Observer模式在Java GUI事件模型中應用
其實在AWT/Swing事件模型中用到了好幾種設計模式,以前的JDK 1.0 AWT使用的是“基於繼承的事件模型”,在該模型Component類中定義了一系列事件處理方法,如:handleEvent,mouseDown,mouseUp等等,我們對事件的響應是通過對組件類繼承並覆蓋相應的事件處理方法的手段來實現,這種模型有很多缺點,事件的處理不應當由事件產生者負責,而且根據“設計模式”一書中的原則,“繼承”通常被認為是“對封裝性的破壞”,父子類之間的緊密耦合關系降低了靈活性,同時繼承容易導致家族樹規模的龐大,這些都不利於組件可重用。
JDK 1.1以後新的事件模型是被成為“基於授權的事件模型”,也就是我們現在所熟悉的Listener模型,事件的處理不再由產生事件的對象負責,而由Listener負責。尤其在Swing組件中設計MVC結構時用到了Observer模式,眾所周知,MVC表示“模型-視圖-控制器”,即“數據-表示邏輯-操作”,其中數據可以對應多種表示,這樣視圖就處在了observer的地位,而model則是subject。
5.簡單的例子
回到本文一開始的那個Explorer的例子,我們考慮做一個簡單的圖片浏覽器,使樹型選擇組件和圖片浏覽面板在兩個不同的類中,其中圖片浏覽面板根據所選擇的樹的節點顯示相應的圖片,所以圖片浏覽面板是一個observer,樹是subject。由於Java單根繼承的原因,我們不能同時繼承JPanel和Observable,但可以用對象的組合把一個subject放到我們的類當中,並通過TreeSelectionListener觸發subject的setChanged方法,並通過notifyObservers方法通知observer。
例子代碼如下:
//LeftPanel.java
package com.jungleford.test;
import java.awt.BorderLayout;
import javax.swing.*;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import java.util.Observable;
import java.util.Observer;
public final class LeftPanel extends JPanel
{// 把樹型選擇視圖布局在左邊
private JTree tree;// 樹型選擇視圖
private JScrollPane scroll;// 讓視圖可滾動
private DefaultMutableTreeNode root, node1, node2;// 根節點及兩個葉子
private Sensor sensor;// sensor是一個Observable,由於只能單根繼承,所以作為組合成員
private String file;// 圖片文件名,與RightPanel通信的內容
public LeftPanel(Observer observer)
{
file = "";
sensor = new Sensor();
sensor.addObserver(observer);// 向Observable注冊Observer
root = new DefaultMutableTreeNode("Images");
tree = new JTree(root);
node1 = new DefaultMutableTreeNode("Rabbit");
node2 = new DefaultMutableTreeNode("Devastator");
root.add(node1);
root.add(node2);
tree.addTreeSelectionListener(new TreeSelectionListener()
{// 樹節點選擇動作
public void valueChanged(TreeSelectionEvent e)
{
Object obj = e.getPath().getLastPathComponent();
if (obj instanceof DefaultMutableTreeNode)
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode)obj;
if (node == root)
file = "";// 選擇根
if (node == node1)
file = "rabbit.jpg";// 選擇node1
if (node == node2)
file = "devastator.gif";// 選擇node2
sensor.setData(file);// 改變Observable
sensor.notifyObservers();// 通知observer,對象已改變
}
}
});
scroll = new JScrollPane(tree);
add(scroll, BorderLayout.CENTER);
}
public Observable getSensor()
{// 返回Observable對象,使Observer可以獲取
return sensor;
}
}
class Sensor extends Observable
{// 定義自己的Observable
private Object data;
public void setData(Object newData)
{
data = newData;
setChanged();// 改變Observable
System.out.println("Data changed!");
}
public Object getData()
{
return data;
}
}
//RightPanel.java
package com.jungleford.test;
import java.awt.*;
import javax.swing.JPanel;
import java.util.Observer;
import java.util.Observable;
public class RightPanel extends JPanel implements Observer
{// 把圖片浏覽視圖布局在右邊
private Image image;
public void update(Observable subject, Object obj)
{// 定義接收到Observable變化後的響應動作
String file = (String)((Sensor)subject).getData();
if (!file.equals(""))
{
image = Toolkit.getDefaultToolkit().getImage(file);
MediaTracker tracker = new MediaTracker(this);// 定義圖像跟蹤
tracker.addImage(image, 0);
try
{
tracker.waitForID(0);// 等待圖像的完全加載
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
else
image = null;
repaint();// 重繪組件
}
public void paintComponent(Graphics g)
{
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, getWidth() - 1, getHeight() - 1);// 先將組件上的畫面清除
if (image != null)
g.drawImage(image, 0, 0, this);// 繪制新的圖像
}
}
//MainFrame.java
package com.jungleford.test;
import java.awt.*;
import javax.swing.JFrame;
public class MainFrame extends JFrame
{// 演示窗口
public static void main(String[] args)
{
MainFrame frame = new MainFrame();
RightPanel right = new RightPanel();
LeftPanel left = new LeftPanel(right);// 注冊Observer
frame.getContentPane().add(left, BorderLayout.WEST);
frame.getContentPane().add(right, BorderLayout.CENTER);
frame.setTitle("Observer Test");
frame.setSize(400, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
程序運行截圖如下:
啟動界面
點擊Rabbit顯示的圖像
點擊Devestator顯示的圖像