設計模式之觀察者模式

觀察者模式又稱爲發佈-訂閱(Publish/Subscribe)模式,是23種設計模式之一。DP中是這麼定義觀察者模式的:java

觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知全部的觀察者對象,使它們可以自動更新本身。設計模式

舉個生活中的例子,例如在某班級裏,幾個同窗都在某個網站上訂閱了一本連載的漫畫。當漫畫更新時,就會通知這幾位同窗,同窗收到通知後就能夠去下載漫畫的最新篇章。這就是一個典型的觀察者模式,在這裏同窗都是觀察者,而漫畫則是他們共同監聽的一個主題,而漫畫更新時也就是主題對象發生變化時,就會通知全部訂閱該漫畫的同窗,因此漫畫也就是通知者。同窗們收到通知後就能夠去下載漫畫的新篇章了,這就是主題對象會通知觀察者對象讓他們進行更新。數組

咱們使用簡單的代碼,來嘗試實現一下這個場景:
漫畫類,也就是主題:bash

package org.zero01.test;

import java.util.Vector;

// 漫畫
public class Cartoon {

    // 學生列表
    private Vector<Student> studentList = new Vector();
    // 漫畫狀態
    private String action;

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    // 添加訂閱該漫畫的學生
    public void attach(Student student) {
        studentList.add(student);
    }

    // 漫畫更新時通知學生
    public void notifyOfStu() {
        for (Student student : studentList) {
            student.update();
        }
    }
}

學生類,也就是觀察者:ide

package org.zero01.test;

public class Student {

    private String name;
    private Cartoon cartoon;

    public Student(String name, Cartoon cartoon) {
        this.name = name;
        this.cartoon = cartoon;
    }

    // 獲得通知時,就去下載新漫畫
    public void update() {
        System.out.println(cartoon.getAction() + ", " + name + "請下載漫畫的新篇章!");
    }

}

客戶端:網站

package org.zero01.test;

public class Client {

    public static void main(String[] args) {

        // 訂閱的漫畫
        Cartoon cartoon=new Cartoon();

        // 訂閱該漫畫的學生
        Student student1=new Student("小明", cartoon);
        Student student2=new Student("小紅", cartoon);

        // 添加訂閱的學生
        cartoon.attach(student1);
        cartoon.attach(student2);

        // 訂閱的漫畫更新了
        cartoon.setAction("您訂閱的XXX漫畫更新啦");
        // 通知訂閱的學生
        cartoon.notifyOfStu();
    }
}

運行結果:this

您訂閱的XXX漫畫更新啦, 小明請下載漫畫的新篇章!
您訂閱的XXX漫畫更新啦, 小紅請下載漫畫的新篇章!

以上的代碼實現了一個簡單的觀察者模式,當漫畫這個主題對象的狀態發生改變時,就會通知全部的訂閱者。設計

咱們編寫的代碼雖然能夠實現以上所說到的場景,可是代碼的耦合性很高,不能徹底符合觀察模式的設計理念。例如,我要增長一個訂閱的是小說類型的學生,那麼就得去修改 「漫畫」 通知者的代碼了。若是我還要增長一個 「小說」 通知者,讓 小說」 通知者也能通知全部學生的話,也須要去修改學生類的代碼,這就不符合開-閉原則了,並且對象之間互相依賴也違背了依賴倒轉原則,以及以上的代碼中沒有編寫取消訂閱的方法也就是減小觀察者的方法。code

既然知道代碼有哪些問題了,那麼咱們就來把這些代碼重構一下:orm

代碼結構圖:
設計模式之觀察者模式

1.首先抽象兩個類:

package org.zero01.test;

// 通知者/主題接口
public interface Subject {
    public void attach(Observer observer);
    public void detach(Observer observer);
    public void notifyOfStu();
    public void setAction(String action);
    public String getAction();
}

package org.zero01.test;

// 抽象觀察者
public interface Observer {

    public abstract void update();

}

2.而後纔是具體的實現類:

漫畫類:

package org.zero01.test;

import java.util.Vector;

// 漫畫主題
public class Cartoon implements Subject{

    // 學生列表
    private Vector<Observer> observerList = new Vector();
    // 主題動做
    private String action;

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    // 添加訂閱該漫畫的學生
    public void attach(Observer observer) {
        observerList.add(observer);
    }

    // 漫畫更新時通知學生
    public void notifyOfStu() {
        for (Observer observer : observerList) {
            observer.update();
        }
    }

    public void detach(Observer observer) {

        observerList.remove(observer);

    }
}

小說類:

package org.zero01.test;

import java.util.Vector;

// 小説主題
public class Story implements Subject{

    // 學生列表
    private Vector<Observer> observerList = new Vector();
    // 主題動做
    private String action;

    public void attach(Observer observer) {
        observerList.add(observer);

    }

    public void detach(Observer observer) {
        observerList.remove(observer);

    }

    public void notifyOfStu() {
        for (Observer observer : observerList) {
            observer.update();
        }

    }

    public void setAction(String action) {
        this.action=action;

    }

    public String getAction() {
        return action;
    }

}

訂閱漫畫的學生:

package org.zero01.test;

public class StuOfCartoon extends Observer{

    private String name;
    private Subject subject;

    public StuOfCartoon(String name, Subject subject) {
        this.name=name; 
        this.subject=subject;
    }

    // 獲得通知時,就去下載新漫畫
    public void update() {
        System.out.println(subject.getAction() + ", " + name + "請下載漫畫的新篇章!");
    }

}

訂閱小說的學生:

package org.zero01.test;

public class StuOfStory extends Observer{

    private String name;
    private Subject subject;

    public StuOfStory(String name, Subject subject) {
        this.name=name; 
        this.subject=subject;
    }

    // 獲得通知時,就去下載小說的新篇章
    public void update() {
        System.out.println(subject.getAction() + ", " + name + "請下載小說的新篇章!");

    }

}

客戶端代碼:

package org.zero01.test;

public class Client {

    public static void main(String[] args) {

        // 漫畫
        Cartoon cartoon = new Cartoon();

        // 訂閱漫畫的學生
        StuOfCartoon student1 = new StuOfCartoon("小明", cartoon);
        StuOfCartoon student2 = new StuOfCartoon("小紅", cartoon);

        // 訂閱小說的學生
        StuOfStory ofStory=new StuOfStory("小剛", cartoon);

        // 添加訂閱的學生
        cartoon.attach(student1);
        cartoon.attach(student2);
        cartoon.attach(ofStory);

        // 取消訂閱,減小訂閱的學生
        cartoon.detach(student2);

        // 訂閱的主題更新了
        cartoon.setAction("您訂閱的XXX更新啦");
        // 通知訂閱的學生
        cartoon.notifyOfStu();

    }

}

運行結果:

您訂閱的XXX更新啦, 小明請下載漫畫的新篇章!
您訂閱的XXX更新啦, 小剛請下載小說的新篇章!

從客戶端的代碼能夠看到,抽象了兩個類以後,即使是隻有一個 」漫畫「 通知者也可以通知訂閱不一樣類型主題的觀察者,而不須要去修改任何的代碼。一樣的,我把 」漫畫「 通知者換成 「小說」 通知者也絲絕不會受到影響:

客戶端代碼:

package org.zero01.test;

public class Client {

    public static void main(String[] args) {

        // 小說
        Story story = new Story();

        // 訂閱漫畫的學生
        StuOfCartoon student1 = new StuOfCartoon("小明", story);
        StuOfCartoon student2 = new StuOfCartoon("小紅", story);

        // 訂閱小說的學生
        StuOfStory ofStory=new StuOfStory("小剛", story);

        // 添加訂閱的學生
        story.attach(student1);
        story.attach(student2);
        story.attach(ofStory);

        // 取消訂閱,減小訂閱的學生
        story.detach(student2);

        // 訂閱的主題更新了
        story.setAction("您訂閱的XXX更新啦");
        // 通知訂閱的學生
        story.notifyOfStu();

    }
}

運行結果:

您訂閱的XXX更新啦, 小明請下載漫畫的新篇章!
您訂閱的XXX更新啦, 小剛請下載小說的新篇章!

這樣的設計就知足了依賴倒轉原則以及開-閉原則,算得上是一個完整的觀察者模式設計的代碼了。

監聽與通知示意圖:
設計模式之觀察者模式

咱們再來看看觀察者模式(Observe)的結構圖:
設計模式之觀察者模式

咱們來使用代碼實現這個結構:

Subject類。該類一般被稱爲主題或抽象通知者,通常使用一個抽象類或者接口進行聲明。它把全部對觀察者對象的引用保存在一個集合裏,每一個主題均可以有任何數量的觀察者。抽象主題提供一個接口,能夠增長和刪除觀察者對象:

import java.util.List;
import java.util.Vector;

public abstract class Subject {

    // 觀察者列表
    private List<Observer> observers = new Vector<Observer>();

    // 增長觀察者
    public void attach(Observer observer) {
        observers.add(observer);
    }

    // 移除觀察者
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    // 通知觀察者
    public void notifyOfObserver() {
        for (Observer observer : observers) {
            observer.update();
        }
    }

}

Observer類,抽象觀察者,爲爲全部的具體觀察者定義一個接口,在獲得主題的通知時更新本身。這個接口叫更新接口。抽象觀察者通常用一個抽象類或者一個接口實現。更新接口一般包含一個update方法,這個方法叫更新方法:

public abstract class Observer {

    public abstract void update();

}

ConcreteSubject類,叫作具體的主題或具體的通知者,該類將有關狀態存入具體的觀察者對象。在具體主題的內部狀態改變時,給全部登記過的觀察者發出通知。具體主題角色一般用一個具體的子類來進行實現:

public class ConcreteSubject extends Subject{

    // 具體主題的狀態
    public String getSubjectState() {
        return subjectState;
    }

    public void setSubjectState(String subjectState) {
        this.subjectState = subjectState;
    }

    private String subjectState;

}

ConcreteObserver類,具體的觀察者類,實現抽象觀察者角色所要求的更新接口,以便使自己的狀態與主題的狀態相協調。具體觀察者角色能夠保存一個指向具體主題對象的引用。具體觀察者角色一般用一個具體的子類進行實現:

public class ConcreteObserver extends Observer {

    // 觀察者名稱
    private String name;
    // 觀察者狀態
    private String observerState;
    private ConcreteSubject concreteSubject;

    public ConcreteObserver(String name, ConcreteSubject concreteSubject) {
        this.name = name;
        this.concreteSubject = concreteSubject;
    }

    public ConcreteSubject getConcreteSubject() {
        return concreteSubject;
    }

    public void setConcreteSubject(ConcreteSubject concreteSubject) {
        this.concreteSubject = concreteSubject;
    }

    // 更新
    public void update() {
        observerState = concreteSubject.getSubjectState();
        System.out.println("觀察者" + name + "的新狀態是" + observerState);
    }
}

客戶端代碼:

public class Client {

    public static void main(String[] args){
        ConcreteSubject concreteSubject=new ConcreteSubject();

        concreteSubject.attach(new ConcreteObserver("A",concreteSubject));
        concreteSubject.attach(new ConcreteObserver("B",concreteSubject));
        concreteSubject.attach(new ConcreteObserver("C",concreteSubject));

        concreteSubject.setSubjectState("test state");
        concreteSubject.notifyOfObserver();
    }

}

運行結果:

觀察者A的新狀態是test state
觀察者B的新狀態是test state
觀察者C的新狀態是test state

觀察者模式特色:

將一個系統分割成一系列互相協做的類有一個很很差的反作用,那就是須要維護相關對象之間的一致性。咱們不但願爲了維持一致性而使各種緊密耦合,這樣會給維護、擴展和複用都帶來不便。而觀察者模式的關鍵對象是主題 Subject 和觀察者Observer ,一個 Subject 能夠有任意數目的依賴它的 Observer ,一旦Subject的狀態發生了改變,全部的Observer 均可以獲得通知。Subject發出通知時並不須要知道誰是它的觀察者,也就是說,具體觀察者是誰,它根本不須要知道。而任何一個具體觀察者不知道也不須要知道其餘觀察者的存在,這樣下降了子類之間的耦合。

何時考慮使用觀察者模式?

1.當一個對象的改變須要同時改變其餘對象的時候,並且它不知道具體有多少個對象有待改變時,應該考慮使用觀察者模式

2.當一個抽象模型有兩個方面,其中一方面依賴於另外一方面,這時用觀察者模式能夠將這二者封裝在獨立的對象中使它們各自獨立地改變和複用。

觀察者模式所作的事情其實就是解耦合,讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響另外一邊的變化。

觀察者模式的不足:

咱們沒辦法讓每一個控件都是實現一個 「Observer」 接口,由於這些控件都早已被它們的製造商封裝好了。並且咱們上面的例子,儘管已經用了依賴倒轉原則,可是 「抽象通知者」 仍是依賴 」抽象觀察者「 ,也就是說,萬一沒有了 」抽象觀察者「 這樣的接口,那麼通知功能就沒法完成了。既然 」通知者「 和 」觀察者「 之間根本就互相不知道,那麼咱們就換另外一種方式,讓客戶端來決定通知誰,這就是接下來要提到的事件委託模式。

事件委託模式的實現

事件委託模式在Java的Swing圖形化中常用,可是在Java語言中沒有對其作必定的封裝,所以實現起來沒那麼容易,不過反射機制學得還不錯的話,其實很好理解實現原理。相比之下C#就容易了不少,C#裏有一個delegate關鍵字,只須要聲明一個委託器就能夠了。在Java中咱們須要本身經過反射機制去實現,正好把上面演示的例子使用事件委託模式進行重構,一會再說明什麼是事件委託:

代碼結構圖:
設計模式之觀察者模式

1.去掉觀察者Observer接口,把兩個具體的觀察者類的代碼修改成以下內容:

package org.zero01.delegate;

import java.util.Date;
// 訂閱漫畫的同窗
public class StuOfCartoon {

    private String name;

    public StuOfCartoon(String name) {
        this.name = name;
    }

    // 獲得通知時,就去下載新漫畫
    public void downloadNewCartoon(Date date) {
        System.out.println(date.toLocaleString() + "  您訂閱的XXX更新啦, " + name + "請下載漫畫的新篇章!");
    }
}

package org.zero01.delegate;

import java.util.Date;
// 訂閱小說的同窗
public class StuOfStory {

    private String name;

    public StuOfStory(String name) {
        this.name = name;
    }

    // 獲得通知時,就去下載小說的新篇章
    public void downloadNewStory(Date date) {
        System.out.println(date.toLocaleString() + "  您訂閱的XXX更新啦, " + name + "請下載小說的新篇章!");

    }
}

2.定義一個事件類,該類經過反射機制完成對觀察者對象方法的調用:

package org.zero01.delegate;

import java.lang.reflect.Method;

/**
* 事件類,經過反射機制調用觀察者對象的方法
*/
public class Event {

    // 要執行方法的對象
    private Object object;
    // 要執行的方法名稱
    private String methodName;
    // 要執行的方法的參數
    private Object[] params;
    // 要執行方法的參數類型
    private Class[] paramTypes;

    public Event() {
    }

    // 初始化屬性
    public Event(Object object, String methodName, Object... params) {
        this.object = object;
        this.methodName = methodName;
        this.params = params;
        contractParamTypes(this.params);
    }

    // 根據參數數組生成參數類型數組
    private void contractParamTypes(Object[] params) {
        this.paramTypes = new Class[params.length];
        for (int i = 0; i < params.length; i++) {
            this.paramTypes[i] = params[i].getClass();
        }
    }

    // 經過反射機制執行該觀察者對象的方法
    public void invoke() throws Exception {
        Method method = object.getClass().getMethod(this.getMethodName(), this.getParamTypes());
        if (method == null) {
            return;
        }
        method.invoke(this.getObject(), this.getParams());
    }

    // 如下都是屬性的setter和getter,就省略了
}

3.事件處理類,該類將事件源信息收集給事件類:

package org.zero01.delegate;

import java.util.ArrayList;
import java.util.List;

/**
 * 事件處理類,收集事件信息交給事件類執行
 */
public class EventHandler {

    private List<Event> objects;

    public EventHandler() {
        objects = new ArrayList<Event>();
    }

    // 添加某個觀察者對象要執行的方法,以及方法所須要的參數
    public void addEvent(Object object, String methodName, Object... params) {
        objects.add(new Event(object, methodName, params));
    }

    // 通知全部的觀察者對象執行指定的方法
    public void notifyOfObserver() throws Exception {
        for (Event event : objects) {
            event.invoke();
        }
    }
}

4.通知者接口:

package org.zero01.delegate;

// 通知者、主題接口
public interface Subject {

    // 增長觀察者,也就是訂閱的學生
    public void addListener(Object object, String methodName, Object... params);
    // 通知學生訂閱的內容更新了
    public void notifyOfObserver();

}

5.具體的通知者:

package org.zero01.delegate;

public class Cartoon implements Subject {

    private EventHandler eventHandler;

    public Cartoon(EventHandler eventHandler) {
        this.eventHandler = eventHandler;
    }

    // 添加事件
    public void addListener(Object object, String methodName, Object... params) {
        eventHandler.addEvent(object, methodName, params);
    }

    // 轉發到事件處理類上
    public void notifyOfObserver() {
        try {
            eventHandler.notifyOfObserver();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Story類的代碼也是同樣的,忽略。

6.客戶端代碼:

package org.zero01.delegate;

import java.util.Date;

public class Client {

    public static void main(String[] args) {

        // 通知者
        Subject cartoon = new Cartoon(new EventHandler());

        // 訂閱漫畫的同窗
        StuOfCartoon stuOfCartoon = new StuOfCartoon("小明");
        // 訂閱小說的同窗
        StuOfStory stuOfStory = new StuOfStory("小紅");

        // 添加觀察者,或者說添加訂閱學生,把兩個不一樣的類以及不一樣的方法委託給事件處理類
        cartoon.addListener(stuOfCartoon, "downloadNewCartoon", new Date());
        cartoon.addListener(stuOfStory, "downloadNewStory", new Date());

        // 發出通知
        cartoon.notifyOfObserver();

    }
}

以上客戶端的代碼能夠看到,爲了方便演示,咱們是經過字符串來傳遞須要執行的方法的名稱。還有另外一種方式就是能夠經過接口去定義方法的名稱,就像Swing中添加點擊事件同樣,須要實現ActionListener接口裏定義的actionPerformed方法,這樣咱們就只須要傳遞觀察者對象便可,而後反射機制就掃這個對象是否有實現接口中定義的方法就能夠了。不過若是不是像點擊事件那種固定不變的方法的話,仍是使用字符串來傳遞須要執行的方法的名稱會好一些,這樣便於修改。

運行結果:

2018-1-29 21:10:30  您訂閱的XXX更新啦, 小明請下載漫畫的新篇章!
2018-1-29 21:10:30  您訂閱的XXX更新啦, 小紅請下載小說的新篇章!

事件委託說明:

如今就能夠來解釋一下,事件委託是什麼了。這就比如我是班長你是班主任,你讓我通知某幾個學生去辦公室,而後我就去通知那幾個學生辦公室,這就是一個委託,你委託的事情是讓我去通知你指定的那幾個學生。而我就是通知者,與觀察者模式不一樣的是,我是由於有你的委託才能去通知學生,而觀察者模式是當主題狀態發生變化時通知觀察者。上面的客戶端代碼裏,咱們將訂閱了相關內容的學生,委託給了通知者,因此通知者就能夠對這些學生髮出通知,但實際調用觀察者方法的是Event類,不是通知者了。

並且一個委託能夠搭載多個方法,這些方法能夠是不一樣類的方法,當發送通知時全部的方法會被依次調用。這樣咱們就不須要在通知者上用一個集合存儲觀察者了,增長、減小觀察者的方法也不須要編寫了,而是轉到客戶端來讓給委託搭載多個方法,這就解決了原本與抽象觀察者耦合的問題。也就是說觀察者模式是由抽象的觀察者來決定調用哪一個方法,而事件委託模式是由客戶端決定調用哪一個方法,這樣通知者就不須要依賴抽象觀察者了。

相關文章
相關標籤/搜索