Java設計模式之觀察者模式

俺就不起網名發表於2017-12-24

一、什麼是觀察者模式

       觀察者模式是物件的行為模式,又叫釋出-訂閱(Publish/Subscribe)模式。觀察者模式定義了一種一對多的依賴關係,讓多個觀察者物件同時監聽某一個主題物件。這個主題物件在狀態上發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己。
舉個例子:一個老師通知三個學生寫作業,只要老師一發作業通知了,三個學生就會收到通知並進行處理。

什麼時候使用觀察者模式:
1、 當一個抽象模型有兩個方面, 其中一個方面依賴於另一方面。將這二者封裝在獨立的物件中以使它們可以各自獨立地改變和複用;
2、當對一個物件的改變需要同時改變其它物件, 而不知道具體有多少物件有待改變;
3、當一個物件必須通知其它物件,而它又不能假定其它物件是誰。換言之, 你不希望這些物件是緊密耦合的;
比如賬戶改變了傳送簡訊;使用者操作了記錄操作行為等;

觀察者模式所涉及的角色有:
  ●  抽象主題(Subject)角色:抽象主題角色把所有觀察者物件的儲存在一個集合裡(比如ArrayList物件),每個主題都可以有任何數量的觀察者。抽象主題提供一個介面,可以增加和刪除觀察者物件,抽象主題角色又叫做抽象被觀察者(Observable)角色。
  ●  具體主題(ConcreteSubject)角色:(抽象主題角色的實現類)將有關狀態存入具體觀察者物件;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。
  ●  抽象觀察者(Observer)角色:為所有的具體觀察者定義一個介面,在得到主題的通知時更新自己,這個介面叫做更新介面。
  ●  具體觀察者(ConcreteObserver)角色:(抽象觀察者角色的實現類)儲存與主題的狀態自恰的狀態。具體觀察者角色實現抽象觀察者角色所要求的更新介面,以便使本身的狀態與主題的狀態相協調。

二、例子

寫老師通知學生寫作業的例子,根據以上觀察者模式角色可知通常有兩個介面及其實現類(抽象主題和觀察者角色介面及兩者的實現類,老師是主題角色,學生是觀察者角色)。下面開始程式碼:

1、抽象觀察者角色介面:提供觀察者接收到通知修改狀態的方法;

/**
 * 觀察者
 */
public interface Observer {
    //當主題傳送通知給觀察者時,觀察者改變狀態
    void update(String info);
}

2、抽象主題角色介面:提供增加/刪除/通知觀察者的方法;

/**
 * 主題介面
 */
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObserver(String info);
}

3、主題介面實現類:提供一個集合存放觀察者,提供一個變數提供資訊給觀察者相應資訊;

/**
 * 主題介面實現類
 */
public class TeacherSubject implements Subject {

    //用來存放和記錄觀察者
    private List<Observer> observers = new ArrayList<Observer>();
    
    //記錄狀態的字串
    private String info;

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        int i = observers.indexOf(observer);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObserver(String info) {
        System.out.println("今天的作業是" + info);
        for (Observer observer : observers) {
            observer.update(info);
        }
    }

}

4、觀察者介面實現類:如果不考慮取消觀察,可以不提供主題角色變數(在構造觀察者物件的時候將觀察者放到集合裡)

/**
 * 觀察者介面實現類
 */
public class StudentObserver implements Observer {
    //儲存一個主題物件的引用,以後如果想取消訂閱,有了這個引用會比較方便
    private TeacherSubject t;
    //學生的姓名,用來標識不同的學生物件
    private String name;

    //構造器用來註冊觀察者
    public StudentObserver(String name, TeacherSubject t) {
        this.name = name;
        this.t = t;
        //每新建一個學生物件,預設新增到觀察者的行列
        t.addObserver(this);
    }

    @Override
    public void update(String info) {
        System.out.println(name + "收到作業:" + info);
    }
}

5、測試類:一個老師,通知三個學生

public class TestObserver {
    public static void main(String[] args) {

        TeacherSubject teacher = new TeacherSubject();
        StudentObserver zhangSan = new StudentObserver("張三", teacher);
        StudentObserver liSi = new StudentObserver("李四", teacher);
        StudentObserver wangWu = new StudentObserver("王五", teacher);

        teacher.notifyObserver("第二頁第六題");
        teacher.notifyObserver("第三頁第七題");
        teacher.notifyObserver("第五頁第八題");
    }
}

以上例子很明顯透露出觀察者模式的一個缺點:迴圈!

三、推模型和拉模型

在觀察者模式中,又分為推模型和拉模型兩種方式。
  ●  推模型
     主題物件向觀察者推送主題的詳細資訊,不管觀察者是否需要,推送的資訊通常是主題物件的全部或部分資料。

●  拉模型
     主題物件在通知觀察者的時候,只傳遞少量資訊。如果觀察者需要更具體的資訊,由觀察者主動到主題物件中獲取,相當於是觀察者從主題物件中拉資料。一般這種模型的實現中,會把主題物件自身通過update()方法傳遞給觀察者,這樣在觀察者需要獲取資料的時候,就可以通過這個引用來獲取了。
    
兩種模式的比較:
推模型是假定主題物件知道觀察者需要的資料;而拉模型是主題物件不知道觀察者具體需要什麼資料,沒有辦法的情況下,乾脆把自身傳遞給觀察者,讓觀察者自己去按需要取值;
推模型可能會使得觀察者物件難以複用,因為觀察者的update()方法是按需要定義的引數,可能無法兼顧沒有考慮到的使用情況。這就意味著出現新情況的時候,就可能提供新的update()方法,或者是乾脆重新實現觀察者;而拉模型就不會造成這樣的情況,因為拉模型下,update()方法的引數是主題物件本身,這基本上是主題物件能傳遞的最大資料集合了,基本上可以適應各種情況的需要。

拉模式具體程式碼區別就是傳的引數,程式碼不提供了。

四、JAVA提供的對觀察者模式的支援

java.util庫裡面,提供了一個Observable類以及一個Observer介面,構成JAVA語言對觀察者模式的支援。
1、Observer介面:
       抽象觀察者介面,該介面只定義了一個方法,即update()方法,當被觀察者物件的狀態發生變化時,被觀察者物件的notifyObservers()方法就會呼叫這一方法;
2、Observable抽象類:
       被觀察者類(主題角色類)都是java.util.Observable類的子類。

使用javaAPI的觀察者模式需要明白這麼幾件事情:
1、如何使物件變為觀察者?
  實現觀察者介面(java.util.Observer),然後呼叫Observable物件的addObserver()方法,不想再當觀察者時,呼叫deleteObserver()就可以了;
2、被觀察者(主題)如何發出通知?
  第一步:先呼叫setChanged()方法,標識狀態已經改變的事實;
  第二步:呼叫notifyObservers()方法或notifyObservers(Object arg),這就牽扯到推(push)和拉(pull)的方式傳送資料。如果想用push的方式"推"資料給觀察者,可以把資料當做資料物件傳送給notifyObservers(Object arg)方法,其中的arg可以為任意物件,意思是你可以將任意物件傳送給每一個觀察者。如果呼叫不帶引數的notifyObserver()方法,則意味著你要使用pull的方式去主題物件中"拉"來所需要的資料;
3、觀察者如何接收通知?
  觀察者只需要實現一個update(Observable o,Object arg)方法,第一個引數o,是指定通知是由哪個主題下達的,第二個引數arg就是上面notifyObserver(Object arg)裡傳入的資料,如果不傳該值,arg為null;

下面使用java內建API實現上面我所寫的老師和學生的例子:
1、主題角色類(繼承Observable類)

public class Teacher extends Observable {
    private String info;

    public void setHomework(String info) {
        this.info = info;
        System.out.println("佈置的作業是" + info);
        setChanged();
        notifyObservers();//這裡使用了拉模式觀察,如果使用推模式可以在改方法傳引數,會在觀察者update方法的第二個引數獲取到
    }

    public String getInfo() {
        return info;
    }
}

2、觀察者物件類(實現Observer介面)

public class Student implements Observer {

    private Observable ob;
    private String name;

    public Student(String name, Observable ob) {
        this.ob = ob;
        this.name = name;
        ob.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        Teacher t = (Teacher) o;
        System.out.println(name + "收到作業資訊:" + t.getInfo());

    }

}

3、測試方法類似,就不寫了

五、優缺點

優點:
1、觀察者模式支援廣播通訊。被觀察者會向所有的登記過的觀察者發出通知;
2、觀察者模式在被觀察者和觀察者之間建立一個抽象的耦合。被觀察者角色所知道的只是一個具體觀察者列表,每一個具體觀察者都符合一個抽象觀察者的介面。被觀察者並不認識任何一個具體觀察者,它只知道它們都有一個共同的介面;
缺點:
1、如果一個被觀察者物件有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間;
2、如果在被觀察者之間有迴圈依賴的話,被觀察者會觸發它們之間進行迴圈呼叫,導致系統崩潰。在使用觀察者模式是要特別注意這一點;

參考文件:
1、https://www.cnblogs.com/fingerboy/p/5468994.html
2、https://www.cnblogs.com/java-my-life/archive/2012/05/16/2502279.html

相關文章