有一個模式可以幫助你的對象知悉現況,不會錯過該對象感興趣的事,對象甚至在運行時可以決定是否要繼續被通知,如果一個對象狀態的改變需要通知很多對這個對象關註的一系列對象,就可以使用觀察者模式 。觀察者模式也是JDK中使用最多的一個設計模式,而我們本章討論的就是它。 那麼首先,我們先來看一看此模式的定義: ...
有一個模式可以幫助你的對象知悉現況,不會錯過該對象感興趣的事,對象甚至在運行時可以決定是否要繼續被通知,如果一個對象狀態的改變需要通知很多對這個對象關註的一系列對象,就可以使用觀察者模式 。觀察者模式也是JDK中使用最多的一個設計模式,而我們本章討論的就是它。
那麼首先,我們先來看一看此模式的定義:
定義:觀察者模式(有時又被稱為發佈-訂閱模式、模型-視圖模式、源-收聽者模式或從屬者模式)是軟體設計模式的一種。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實作事件處理系統
接下來請LZ用一個例子展出今天的內容:
ex:有這樣一個需求,劉能訂閱了報刊的報紙,以後每周只要有新報紙發佈,劉能就能得到報紙,而劉能也可以選擇退訂,這時將不再得到報紙。報刊(Newspapers) ,訂閱者劉能(Subscriber)。
我們來分析一下,首先,訂閱者可以訂閱報刊,也可以退訂,所以我們需要寫兩個方法,分別為 registerObserver(訂閱) ,removeObserver(退訂),我們用一個list來存訂閱者,
public class Newspapers {//報刊
private List<Subscriber> subscriber = new ArrayList<Subscriber>();
private String paper;
public void registerObserver(Subscriber subs){//訂閱
subscriber.add(subs);
}
public void removeObserver(Subscriber subs){//退訂
subscriber.remove(subs);
}
public void setPaper(String paper){//這裡控制新出報紙
this.paper = paper;
measurementsChanged();//一旦有新報紙出現調用此方法
}
public void measurementsChanged(){
subscriber.get(0).update(paper);
}
}
在訂閱者中,我們設計成只訂閱不退訂,我們需要一個Newspapers對象,用來訂閱,
class Subscriber{//訂閱者劉能
private String paper;
private Newspapers newspapers;
public Subscriber(Newspapers newspapers) {
this.newspapers = newspapers;
newspapers.registerObserver(this);
}
public void update(String paper){
this.paper = paper;
display();
}
public void display(){
System.out.println("paper :"+paper);
}
}
接著我們寫一個測試類
public class Test {
public static void main(String[] args) {
Newspapers newspapers = new Newspapers();
Subscriber subscriber = new Subscriber(newspapers);
newspapers.setPaper("今日說法");
}
}
測試結果:報刊出現新的報紙,然後提醒它的所有訂閱者
這個功能到此就算是實現完了,但是,這時候我們接到了報社的電話:報社告訴我們謝大腳和王小蒙也訂閱了報紙,當報社有新的報紙時我們不僅要給劉能發,還要給謝大腳和王小蒙也發一份=。=
我們發現,這時我們需要再分別寫兩個類來代表謝大腳和王小蒙,但是Newspapers類中我們怎麼來寫?我們的訂閱和註冊已經寫死了只支持劉能!其實訂閱報紙本就是一個一對多的關係,而我們經過了上一章的學習,發現這種實現方式有很多地方是不對的,針對具體實現編程,會導致我們以後在訂閱或退訂時必須修改程式,所以我們應該想到使用介面。在這裡,我們把報刊稱為“主題”(Subject),而訂閱者稱為
“觀察者(Observer)”,讓我們來看的更仔細點:
主題和觀察者定義了一對多的關係,觀察者依賴於此主題,只要主題狀態一有改變,觀察者就會被通知。根據通知的風格,觀察者可能因此而更新。
① 我們定義一個主題介面Subject,它除了註冊和撤銷方法之外,還擁有notifyObservers()方法,此方法用於在狀態改變時更新所有當前觀察者。對象只有使用此介面註冊為觀察者,或者把自己從觀察者中刪除。
② 接下來我們定義一個觀察者介面Observer,所有潛在的觀察者必須實現此介面,它只擁有一個公共方法update,當主題狀態改變時,它被調用。具體的觀察者可以是實現此介面的任意類,觀察者必須註冊具體主題,以便接受更新。
③此外,我們還需要一個介面DisplayElement用來顯示
下麵我們來實現一下:
public interface Subject{
public void registerObserver(Observer observer);//註冊觀察者
public void removeObserver(Observer observer);//移除觀察者
public void notifyObservers();//當主題狀態改變,調用這個方法通知觀察者
}
public interface Observer {
public void update(String paper);//此方法當產生改變的時候由主題調用,
}
public interface DisplayElement{
public void display();//顯示
}
下麵是主題實現類:
//主題(報社)
public class Newspapers implements Subject{
private ArrayList<Observer> observers ;
private String paper;
public Newspapers() {
observers = new ArrayList<Observer>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
int index = observers.indexOf(observer);
if(index >= 0){
observers.remove(index);
}
}
public void setMeasurements(String paper){
this.paper = paper;
measurementsChanged();//當有新報紙時調用此方法
}
public void measurementsChanged(){
notifyObservers();
}
@Override
public void notifyObservers() {//調用此方法通知所有觀察者
for(int i=0;i<observers.size();i++){
Observer observer = observers.get(i);
observer.update(paper);//更新報紙
}
}
}
觀察者:
//觀察者劉能
public class SubscriberLiuNeng implements Observer,DisplayElement{
private String paper;
private Subject subject;
public SubscriberLiuNeng(Subject subject) {
this.subject = subject;
subject.registerObserver(this);
}
@Override
public void update(String paper) {
this.paper = paper;
display();
}
public void display(){
System.out.println("劉能,"+paper+"新報紙出來了");
}
}
//觀察者謝大腳
class SubscriberXieDaJiao implements Observer,DisplayElement{
private String paper;
private Subject subject;
public SubscriberXieDaJiao(Subject subject) {
this.subject = subject;
subject.registerObserver(this);
}
@Override
public void update(String paper) {
this.paper = paper;
display();
}
public void display(){
System.out.println("謝大腳,"+paper+"新報紙出來了");
}
}
//觀察者王小蒙
class SubscriberWangXiaoMeng implements Observer,DisplayElement{
private String paper;
private Subject subject;
public SubscriberWangXiaoMeng(Subject subject) {
this.subject = subject;
subject.registerObserver(this);
}
@Override
public void update(String paper) {
this.paper = paper;
display();
}
public void display(){
System.out.println("王小蒙,"+paper+"新報紙出來了");
}
}
這裡我們保存subject引用是為瞭如果想要取消註冊會非常方便。接著我們寫個測試類:
public static void main(String[] args) {
Newspapers newspapers = new Newspapers();
SubscriberLiuNeng subscriberLiuNeng = new SubscriberLiuNeng(newspapers);
SubscriberXieDaJiao subscriberXieDaJiao = new SubscriberXieDaJiao(newspapers);
SubscriberWangXiaoMeng subscriberWangXiaoMeng = new SubscriberWangXiaoMeng(newspapers);
newspapers.setMeasurements("今日說法");
}
結果:
我們可以發現,通過這種實現方式,主題和觀察者之間依然可以互相交互,但是並不清除彼此的細節。關於觀察者的一切,主題只知道觀察者實現了某個介面(也就是Observer介面)。主題不需要知道觀察者的具體類是誰,做了些神馬或其他任何細節。任何時候我們都可以增加新的觀察者,因為主題唯一依賴的東西是一個實習Observer介面的對象列表,所以我們可以隨時增加觀察者。事實上,在運行時我們可以用新的觀察者取代現有的觀察者,主題不會受到任何影響,同樣的,也可以在任何時候刪除某些觀察者。
當有新類型的觀察者出現時,主題的代碼不需要修改,假如我們有個新的具體類需要
當觀察者,我們不需要為了相容新類型而修改主題的代碼,所有要做的就是在新的類里實現此觀察者介面,然後註冊為觀察者即可。主題不在乎別的,它只會發送通知給所有實現了觀察者藉口的對象。
我們可以獨立地復用主題或觀察者,如果我們在其他地方需要使用主題或觀察者,可以輕易地復用,因為二者並非緊耦合。下麵我們來看一下觀察者模式的類圖,是不是很熟悉?
回到上面的例子,其實這是一個“推”的例子,數據是由主題推給觀察者的,而不是由觀察者自己獲取,其實Java有自己內置的Observer模式不僅支持“推”,還支持“拉”。
java.util包內包含最基本的Observer介面(相當於上面的Subject介面)與Observable類(相當於上面寫的Observer介面)
而java內置的觀察者模式運作方式,和我們的實現類似,但有一些小差異,其中最明顯的差異是Newspapers(也就是我們的主題)現在擴展自Observable類,並繼承到一些增加,刪除,通知觀察者的方法(以及其他的方法),廢話不多說,直接上Observable類源碼:
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) {
/*
* a temporary array buffer, used as a snapshot of the state of
* current Observers.
*/
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();
}
}
註意setChanged()方法用來標記狀態已經改變的事實,好讓notifyObservers()知道當它被調用時應該更新觀察者。如果在notifyObservers()之前沒有先調用setChanged()方法,那麼觀察者就不會被通知。這樣做有其必要性,setChanged()方法可以讓你在更新觀察者時,有更多的彈性,你可以更適當的通知觀察者。比方說,我們的報社是如此的敏銳,以致於報紙剛寫了十分之一就會更新,這會導致Newspaper對象持續不斷的通知觀察者,我們顯然不願意這種情況發生,我們希望做到的是當報紙剛剛印刷出來,我們才更新,就可以在報紙剛印刷出的時候調用setChanged()方法,進而有效的更新。
下麵是我們使用java內置的觀察者模式實現:
import java.util.ArrayList;
import java.util.Observable;
//主題
public class Newspapers extends Observable{
private String paper;
public void setMeasurements(String paper){
this.paper = paper;
measurementsChanged();//當有新報紙時調用此方法
}
public void measurementsChanged(){
setChanged();
notifyObservers();
}
public String getPaper() {
return paper;
}
}
這裡我們不需要創建list數據結構來存儲觀察者了,而在measurementsChanged()方法中我們不需要調用notifyObservers()方法來傳送數據對象,因為這次我們採用的是“拉”的做法,由觀察者自己獲取。所以我們為此也提供了getPaper方法。
接下來看觀察者:
//觀察者劉能
public class SubscriberLiuNeng implements Observer,DisplayElement{
private String paper;
private Observable observable;
public SubscriberLiuNeng(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
@Override
public void update(Observable obs, Object arg) {
if(obs instanceof Newspapers){
Newspapers newspapers =(Newspapers) obs;
this.paper = newspapers.getPaper();
display();
}
}
public void display(){
System.out.println("劉能,"+paper+"新報紙出來了");
}
}
//觀察者劉能
class SubscriberXieDaJiao implements Observer,DisplayElement{
private String paper;
private Observable observable;
public SubscriberXieDaJiao(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
@Override
public void update(Observable obs, Object arg) {
if(obs instanceof Newspapers){
Newspapers newspapers =(Newspapers) obs;
this.paper = newspapers.getPaper();
display();
}
}
public void display(){
System.out.println("謝大腳,"+paper+"新報紙出來了");
}
}
//觀察者劉能
class SubscriberWangXiaoMeng implements Observer,DisplayElement{
private String paper;
private Observable observable;
public SubscriberWangXiaoMeng(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
@Override
public void update(Observable obs, Object arg) {
if(obs instanceof Newspapers){
Newspapers newspapers =(Newspapers) obs;
this.paper = newspapers.getPaper();
display();
}
}
public void display(){
System.out.println("王小蒙,"+paper+"新報紙出來了");
}
}
可以看到,在update方法中,我們先確定可觀察者屬於Newspapers類型,然後調用get方法獲取paper接下來利用display()方法顯示出來。測試類與之前一樣,但是為了能讓大家看的明白點,這裡我再寫一遍:
public static void main(String[] args) {
Newspapers newspapers = new Newspapers();
SubscriberLiuNeng subscriberLiuNeng = new SubscriberLiuNeng(newspapers);
SubscriberXieDaJiao subscriberXieDaJiao = new SubscriberXieDaJiao(newspapers);
SubscriberWangXiaoMeng subscriberWangXiaoMeng = new SubscriberWangXiaoMeng(newspapers);
newspapers.setMeasurements("今日說法");
}
註意看結果:
這是怎麼回事?文字輸出順序居然不一樣了。這其實是因為Observable實現了它的notifyObservers()方法,這導致了通知觀察者的次序不同於我們先前的次序,其實誰都沒有錯,只是雙方選擇不同的方式實現罷了。
但是,如果我們的代碼依賴這樣的次序,就是錯的,為什麼呢?因為一旦觀察者/可觀察者的實現有所改變,通知次序就會改變,很可能產生錯誤的結果,這絕對不是我們所認為的松耦合。
java.util.Observable的黑暗面
想必你早已註意到了,可觀察者是一個“類”而不是一個“介面”,更糟的是,它甚至沒有實現一個介面。而且,它的實現有許多問題,限制了它的使用和復用,雖然它提供了有用的功能,但是LZ依然想提醒大家註意一個事實:Observable是一個類
這違背了我們的原則,會造成什麼問題呢?你必須設計一個類去繼承它,如果某類想同時具有Observable類和另一個基類的行為,就會陷入兩難,因為java不支持多繼承。再者,因為沒有Observable介面,你無法建立自己的實現,和java內置的ObserverAPI搭配使用,也無法將java.util的實現換成另一套做法的實現(比方說,Observable將關鍵的方法保護起來,通過上面LZ放出的源碼,你會發現setChanged()方法是protected類型的)。這意味著:除非你繼承自Observable,否則你無法創建Observable示例並組合到自己的對象中,這違反了我們的設計原則:多用組合,少用繼承。
所以,如果你能夠擴展Observable,那麼它“可能”可以符合你的要求,否則,你就需要像LZ一開始那樣自己手動實現這一整套觀察者模式,不過都無所謂,不管使用哪一種方法,我們都已經熟悉了觀察者模式了。
通過LZ的講解,想必各位都已經明瞭了觀察者模式的具體實現方式,也清楚了它是如何做到解耦的。觀察者模式定義了對象之間的一對多依賴,這樣依賴,當一個對象改變狀態時,它的所有依賴者都會收到通知並自動更新,另外,觀察者模式分離了觀察者和被觀察者二者的責任,這樣讓類之間各自維護自己的功能,專註於自己的功能,會提高系統的可維護性和可重用性.
下麵我們來升華一下觀察者模式。在JDK中有這樣一個類PropertyChangeSupport,
它用來監聽bean的屬性是否發生改變,當bean的屬性發生變化時,使用PropertyChangeSupport對象的firePropertyChange方法,它會將一個事件發送給所有已經註冊的監聽器。該方法有三個參數:屬性的名字、舊的值以及新的值。屬性的值必須是對象,如果是簡單數據類型,則必須進行包裝。我們來看一下PropertyChangeSupport的源代碼(由於源代碼過長,LZ只裁取了一部分這裡有用的)
public class PropertyChangeSupport implements Serializable {
private PropertyChangeListenerMap map = new PropertyChangeListenerMap();
public PropertyChangeSupport(Object sourceBean) {
if (sourceBean == null) {
throw new NullPointerException();
}
source = sourceBean;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
if (listener instanceof PropertyChangeListenerProxy) {
PropertyChangeListenerProxy proxy =
(PropertyChangeListenerProxy)listener;
// Call two argument add method.
addPropertyChangeListener(proxy.getPropertyName(),
proxy.getListener());
} else {
this.map.add(null, listener);
}
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
if (listener instanceof PropertyChangeListenerProxy) {
PropertyChangeListenerProxy proxy =
(PropertyChangeListenerProxy)listener;
// Call two argument remove method.
removePropertyChangeListener(proxy.getPropertyName(),
proxy.getListener());
} else {
this.map.remove(null, listener);
}
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return this.map.getListeners();
}
public void addPropertyChangeListener(
String propertyName,
PropertyChangeListener listener) {
if (listener == null || propertyName == null) {
return;
}
listener = this.map.extract(listener);
if (listener != null) {
this.map.add(propertyName, listener);
}
}
public void removePropertyChangeListener(
String propertyName,
PropertyChangeListener listener) {
if (listener == null || propertyName == null) {
return;
}
listener = this.map.extract(listener);
if (listener != null) {
this.map.remove(propertyName, listener);
}
}
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return this.map.getListeners(propertyName);
}
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
firePropertyChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue));
}
}
public void firePropertyChange(PropertyChangeEvent event) {
Object oldValue = event.getOldValue();
Object newValue = event.getNewValue();
if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
String name = event.getPropertyName();
PropertyChangeListener[] common = this.map.get(null);
PropertyChangeListener[] named = (name != null)
? this.map.get(name)
: null;
fire(common, event);
fire(named, event);
}
}
private static void fire(PropertyChangeListener[] listeners, PropertyChangeEvent event) {
if (listeners != null) {
for (PropertyChangeListener listener : listeners) {
listener.propertyChange(event);
}
}
}
}
LZ帶著大家來一步步分析源碼,從源碼中不難看出map的類型是<String,PropertyChangeListener>,而這個PropertyChangeListener我們稍後說。首先構造器不用多說了,獲取一個bean。而addPropertyChangeListener方法大家有沒有覺得眼熟?這個就相當於我們的註冊,PropertyChangeListenerProxy是PropertyChangeListener的實現類
而PropertyChangeListener相當於觀察者介面,我們另觀察者實現此介面或者繼承PropertyChangeListenerProxy類都可以,removePropertyChangeListener相當於撤銷,下麵我們看最重要的方法firePropertyChange(),這裡的oldSource是改變之前的屬性值,newValue是改變後的屬性值,而propertyName相當於屬性名,也就是key。這裡將參數封裝到了PropertyChangeEvent類中調用firePropertyChange(PropertyChangeEvent event)方法,我們發現,最後遍歷所有的觀察者,調用觀察者的propertyChange()方法,而這個方法是PropertyChangeListener 介面中的,不管我們採用實現介面還是繼承PropertyChangeListenerProxy的方式,都需要我們親自實現這個方法。
public interface PropertyChangeListener extends java.util.EventListener {
/**
* This method gets called when a bound property is changed.
* @param evt A PropertyChangeEvent object describing the event source
* and the property that has changed.
*/
void propertyChange(PropertyChangeEvent evt);
}
下麵我們來寫一個demo:
建立一個MyBean,相當於我們說的主題,我們利用PropertyChangeSupport構造器將bean對象傳入。
public class MyBean{
private String source = "hello";
private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
public void setSource(String newSource) {
String oldSource = source;
source = newSource;
propertyChangeSupport.firePropertyChange("source", oldSource, newSource);
}
public String getSource() {
return source;
}
public void addPropertyChangeListener(PropertyChangeListener listener){
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener){
propertyChangeSupport.removePropertyChangeListener(listener);
}
}
這裡需要我們手動實現addPropertyChangeListener和removePropertyChangeListener方法,因為我們的主題並沒有通過繼承其他類而獲得這兩個方法。
下麵我們寫出測試類來監聽主題的Source屬性是否改變:
public class ChangeListener implements PropertyChangeListener{
public static void main(String[] args) {
MyBean mybean = new MyBean();
mybean.addPropertyChangeListener(new ChangeListener());
mybean.setSource("WOW");
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
MyBean mybean = (MyBean) evt.getSource();
if(evt.getPropertyName().equals(mybean.getSource()));
System.out.println("BeanTest 的 name 屬性變化!");
}
}
結果:
下期預告:裝飾者模式