设计模式之观察者模式

2020-09-23 12:49:36 浏览数 (1)

观察者模式又称为发布-订阅(Publish/Subscribe)模式,是23种设计模式之一。DP中是这么定义观察者模式的:

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。

举个生活中的例子,例如在某班级里,几个同学都在某个网站上订阅了一本连载的漫画。当漫画更新时,就会通知这几位同学,同学收到通知后就可以去下载漫画的最新篇章。这就是一个典型的观察者模式,在这里同学都是观察者,而漫画则是他们共同监听的一个主题,而漫画更新时也就是主题对象发生变化时,就会通知所有订阅该漫画的同学,所以漫画也就是通知者。同学们收到通知后就可以去下载漫画的新篇章了,这就是主题对象会通知观察者对象让他们进行更新。

我们使用简单的代码,来尝试实现一下这个场景: 漫画类,也就是主题:

代码语言:javascript复制
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();
        }
    }
}

学生类,也就是观察者:

代码语言:javascript复制
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   "请下载漫画的新篇章!");
    }

}

客户端:

代码语言:javascript复制
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();
    }
}

运行结果:

代码语言:javascript复制
您订阅的XXX漫画更新啦, 小明请下载漫画的新篇章!
您订阅的XXX漫画更新啦, 小红请下载漫画的新篇章!

以上的代码实现了一个简单的观察者模式,当漫画这个主题对象的状态发生改变时,就会通知所有的订阅者。

我们编写的代码虽然可以实现以上所说到的场景,但是代码的耦合性很高,不能完全符合观察模式的设计理念。例如,我要增加一个订阅的是小说类型的学生,那么就得去修改 “漫画” 通知者的代码了。如果我还要增加一个 “小说” 通知者,让 小说” 通知者也能通知所有学生的话,也需要去修改学生类的代码,这就不符合开-闭原则了,而且对象之间互相依赖也违背了依赖倒转原则,以及以上的代码中没有编写取消订阅的方法也就是减少观察者的方法。

既然知道代码有哪些问题了,那么我们就来把这些代码重构一下:

代码结构图:

1.首先抽象两个类:

代码语言:javascript复制
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.然后才是具体的实现类:

漫画类:

代码语言:javascript复制
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);

    }
}

小说类:

代码语言:javascript复制
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;
    }

}

订阅漫画的学生:

代码语言:javascript复制
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   "请下载漫画的新篇章!");
    }

}

订阅小说的学生:

代码语言:javascript复制
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   "请下载小说的新篇章!");

    }

}

客户端代码:

代码语言:javascript复制
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();

    }

}

运行结果:

代码语言:javascript复制
您订阅的XXX更新啦, 小明请下载漫画的新篇章!
您订阅的XXX更新啦, 小刚请下载小说的新篇章!

从客户端的代码可以看到,抽象了两个类之后,即便是只有一个 ”漫画“ 通知者也能够通知订阅不同类型主题的观察者,而不需要去修改任何的代码。同样的,我把 ”漫画“ 通知者换成 “小说” 通知者也丝毫不会受到影响:

客户端代码:

代码语言:javascript复制
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();

    }
}

运行结果:

代码语言:javascript复制
您订阅的XXX更新啦, 小明请下载漫画的新篇章!
您订阅的XXX更新啦, 小刚请下载小说的新篇章!

这样的设计就满足了依赖倒转原则以及开-闭原则,算得上是一个完整的观察者模式设计的代码了。

监听与通知示意图:

我们再来看看观察者模式(Observe)的结构图:

我们来使用代码实现这个结构:

Subject类。该类通常被称为主题或抽象通知者,一般使用一个抽象类或者接口进行声明。它把所有对观察者对象的引用保存在一个集合里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象:

代码语言:javascript复制
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方法,这个方法叫更新方法:

代码语言:javascript复制
public abstract class Observer {

    public abstract void update();

}

ConcreteSubject类,叫做具体的主题或具体的通知者,该类将有关状态存入具体的观察者对象。在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色通常用一个具体的子类来进行实现:

代码语言:javascript复制
public class ConcreteSubject extends Subject{

    // 具体主题的状态
    public String getSubjectState() {
        return subjectState;
    }

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

    private String subjectState;

}

ConcreteObserver类,具体的观察者类,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。具体观察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体的子类进行实现:

代码语言:javascript复制
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);
    }
}

客户端代码:

代码语言:javascript复制
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();
    }

}

运行结果:

代码语言:javascript复制
观察者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接口,把两个具体的观察者类的代码修改为如下内容:

代码语言:javascript复制
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.定义一个事件类,该类通过反射机制完成对观察者对象方法的调用:

代码语言:javascript复制
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.事件处理类,该类将事件源信息收集给事件类:

代码语言:javascript复制
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.通知者接口:

代码语言:javascript复制
package org.zero01.delegate;

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

    // 增加观察者,也就是订阅的学生
    public void addListener(Object object, String methodName, Object... params);
    // 通知学生订阅的内容更新了
    public void notifyOfObserver();

}

5.具体的通知者:

代码语言:javascript复制
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.客户端代码:

代码语言:javascript复制
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方法,这样我们就只需要传递观察者对象即可,然后反射机制就扫这个对象是否有实现接口中定义的方法就可以了。不过如果不是像点击事件那种固定不变的方法的话,还是使用字符串来传递需要执行的方法的名称会好一些,这样便于修改。

运行结果:

代码语言:javascript复制
2018-1-29 21:10:30  您订阅的XXX更新啦, 小明请下载漫画的新篇章!
2018-1-29 21:10:30  您订阅的XXX更新啦, 小红请下载小说的新篇章!

事件委托说明:

现在就可以来解释一下,事件委托是什么了。这就好比我是班长你是班主任,你让我通知某几个学生去办公室,然后我就去通知那几个学生办公室,这就是一个委托,你委托的事情是让我去通知你指定的那几个学生。而我就是通知者,与观察者模式不同的是,我是因为有你的委托才能去通知学生,而观察者模式是当主题状态发生变化时通知观察者。上面的客户端代码里,我们将订阅了相关内容的学生,委托给了通知者,所以通知者就可以对这些学生发出通知,但实际调用观察者方法的是Event类,不是通知者了。

而且一个委托可以搭载多个方法,这些方法可以是不同类的方法,当发送通知时所有的方法会被依次调用。这样我们就不需要在通知者上用一个集合存储观察者了,增加、减少观察者的方法也不需要编写了,而是转到客户端来让给委托搭载多个方法,这就解决了本来与抽象观察者耦合的问题。也就是说观察者模式是由抽象的观察者来决定调用哪个方法,而事件委托模式是由客户端决定调用哪个方法,这样通知者就不需要依赖抽象观察者了。

0 人点赞