详解Java设计模式之观察者模式(Observer Pattern)

3,807 阅读12分钟

当前流行的 MVC(Model/View/Controller,模型/视图/控制器)架构中也应用了观察者模式,如下图所示。

在这里插入图片描述模型层Model提供的数据是视图层View所观察的对象,在视图层中包含了两个数据显示图表对象,一个是柱状图,一个是饼状图,同样的数据可能有不同的图表显示方式,如果模型层的数据发生改变,则两个图表对象将跟随着发生改变。

这意味着图表对象依赖模型层提供的数据对象,因此数据对象的任何状态改变都应立即通知它们。但是这两个图表之间相互独立,不存在任何联系,而且图表对象的个数没有任何限制,用户可以根据需要再增加新的图表对象,如折线图。也就是说,相同的数据可以对应任意多个图表对象,而且还可以根据需要增加新的图表对象,增加新的图表对象时,对原有系统几乎没有任何影响,满足开闭原则的要求。

建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。


模式介绍

观察者模式主要解决一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作,通过上面的例子我们对该模式已经有了一定的认知,如果要一句话来解释观察者模式呢,大概就是:

  • 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

观察者模式又叫做发布-订阅(Publish/Subscribe)模式,模型-视图( Model/View)模式、源-监听器( Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。


模式结构

观察者模式结构较为复杂,它包括观察目标观察者两个层次结构,

在这里插入图片描述

  1. Subject(目标)

目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,它可以存储任意数量的观察者对象,它提供一个接口来增加和删除观察者对象,同时它定义了的通知方法notifyO。目标类可以是接口,也可以是抽象类或实现类。

  1. ConcreteSubject(具体目标)

具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。

  1. Observer(观察者)

观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。

  1. ConcreteObserver(具体观察者)

在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的观察者集合中或通过detach()方法将自己从目标类的观察者集合中删除。


模式应用场景

观察者模式在软件开发中应用非常广泛,如某电子商务网站可以在执行发送操作时给多个用户发送商品打折信息,某团队战斗游戏中队友牺牲将给所有成员提示等,凡是涉及一对一或者一对多的对象交互场景都可以使用观察者模式

例如:JDK 1.0及更早的AWT事件模型基于职责链模式,但是这种模型不适用于复杂的系统,因此在JDK 1.1版本及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(Delegation Event Model,DEM)。

在这里插入图片描述

在DEM模型里面,目标角色(Subject)负责发布事件,而观察者角色(Observer)可以向目标订阅它所感兴趣的事件。当一个具体目标产生一个事件时,它将通知所有订阅者。在DEM 中 ,事件的发布者称为事件源(Event Source),而订阅者叫做事件监听器(EventListener),在这个过程中还可以通过事件对象(Event Object)来传递与事件相关的信息。可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。事件源对象、事件监听对象(事件处理对象)和事件对象构成了Java事件处理模型的三要素。


练习案例:猫,狗与老鼠

假设猫是老鼠和狗的观察目标,老鼠和狗是观察者,猫叫老鼠跑,狗也跟着叫,使用观察者模式描述该过程。

在这里插入图片描述

  1. 抽象目标类 MySubject

MySubject是抽象目标类,在其中定义了一个ArrayList类型的集合observers,用于存储观察者对象,并定义了注册方法attach()和注销方法detach() ,同时声明了抽象的通知方法 cry()。需要注意的是attach()方法和detach()方法都必须针对抽象观察者进行编程,任何抽象观察者的子类对象都可以注册或注销。

package observer.test1;

import java.util.ArrayList;

/**
 * @author mengzhichao
 * @create 2021-12-04-12:17
 */
public abstract class MySubject {

    protected ArrayList observers =new ArrayList();

    //注册方法
    public void attach(MyObserver observer){
        observers.add(observer);
    }

    //注销方法
    public void detach(MyObserver observer){
        observers.remove(observer);
    }

    public abstract void cry(); //抽象通知方法

}
  1. 抽象观察者类 MyObserver

抽象观察者MyObserver定义为一个接口,在其中声明了抽象响应方法response()

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-12:20
 */
public interface MyObserver {
    void response(); //抽象响应方法
}
  1. 具体目标类 Cat(猫类)

Cat是目标类MySubject 的子类,它实现了抽象方法 cry() ,在cry()中遍历了观察者集合,调用每一个观察者对象的response()响应方法。

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-12:19
 */
public class Cat extends MySubject {
    @Override
    public void cry() {
        System.out.println("猫叫!");
        System.out.println("-----------------------------------------");

        for (Object obs:observers){
            ((MyObserver)obs).response();
        }
    }
}
  1. 具体观察者类 Mouse(老鼠类)

Mouse是具体观察者类,它实现了在抽象观察者中定义的响应方法response()

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-13:04
 */
public class Mouse implements MyObserver {
    @Override
    public void response() {
        System.out.println("老鼠努力逃跑!");
    }
}
  1. 具体观察者类 Dog(狗类)

Dog也是具体观察者类,它实现了在抽象观察者中定义的响应方法response()

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-13:05
 */
public class Dog implements MyObserver {
    @Override
    public void response() {
        System.out.println("狗跟着叫!");
    }
}
  1. 编写客户端类并测试

在客户端代码中需要实例化具体目标类和具体观察者类,先调用目标对象的attach()方法来注册观察者,再调用目标对象的cry()方法,在cry()方法的内部将调用观察者对象的响应方法。

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-13:05
 */
public class Client {
    public static void main(String[] args) {
        MySubject subject=new Cat();

        MyObserver obs1,obs2,obs3;
        obs1=new Mouse();
        obs2=new Mouse();
        obs3=new Dog();

        subject.attach(obs1);
        subject.attach(obs2);
        subject.attach(obs3);

        subject.cry();
    }
}

在这里插入图片描述 观察者模式很好地体现了面向对象设计原则中的开闭原则

如果需要增加一个观察者,如猪也作为猫的观察者,但是猫叫猪无须有任何反应,只需要增加一个新的具体观察者类Pig,而对原有的类库无须做任何改动,这对于系统的扩展性和灵活性有很大提高。

新增的具体观察者类Pig的代码如下:

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-13:13
 */
public class Pig implements MyObserver {
    @Override
    public void response() {
        System.out.println("猪没有反应!");
    }
}

在客户端代码中可以定义一个Pig实例,再将它注册到目标对象的观察者集合中,则需要增加如下代码:

package observer.test1;

/**
 * @author mengzhichao
 * @create 2021-12-04-13:05
 */
public class Client {
    public static void main(String[] args) {
        MySubject subject=new Cat();

        MyObserver obs1,obs2,obs3,obs4;
        obs1=new Mouse();
        obs2=new Mouse();
        obs3=new Dog();
        obs4=new Pig();

        subject.attach(obs1);
        subject.attach(obs2);
        subject.attach(obs3);
        subject.attach(obs4);

        subject.cry();
    }
}

在这里插入图片描述

从本实例可以看出增加新的具体观察者很容易,原有类库代码无须进行任何修改。 在实际使用时,还需要注意以下几个问题

  1. 在客户端尽量针对抽象目标和抽象观察者编程,可以将具体观察者类的类名存储在配置文件中,如果需要更换或增加具体观察者对象只需要修改配置文件即可。如果目标对象和观察者对象之间是一对一关系,则实现过程比较简单,但是如果目标和观察者是一对多关系,则实现过程相对较为复杂。
  2. 在本实例中,由于具体观察者与具体目标类之间没有关联关系,因此增加新的具体目标类也非常方便,只需要扩展抽象目标类即可,而且也可以通过配置文件来存储具体目标类的类名,提高系统的灵活性和可扩展性。
  3. 如果具体观察者与具体目标类之间存在关联关系,则增加新的具体目标类会比较复杂,如果原有观察者类需要访问新增加的具体目标类中的状态,需要修改原有观察者类的源代码,系统的扩展性受到一定影响,不符合“开闭原则”的要求。

总结

观察者模式定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

观察者模式适用情况包括:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面;一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变;一个对象必须通知其他对象,而并不知道这些对象是谁;需要在系统中创建一个触发链。

观察者模式包含四个角色:

  • 目标又称为主题,它是指被观察的对象﹔具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知﹔观察者将对观察目标的改变做出反应﹔在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致。

观察者模式的主要优点在于可以实现表示层和数据逻辑层的分离,并在观察目标和观察者之间建立一个抽象的耦合,支持广播通信;其主要缺点在于如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间,而且如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。


模式扩展(Java语言提供的对观察者模式的支持)

  • 观察者模式在Java语言中的地位非常重要。在JDK的 java.util包中,提供Observable类以及Observer接口,它们构成了Java语言对观察者模式的支持。

在这里插入图片描述

  1. Observer接口

java.util. Observer接口只定义一个方法,它充当抽象观察者,其方法定义代码如下

void update(Observable o,object arg);

当观察目标的状态发生变化时,该方法将会被调用,在Observer的实现子类中实现该update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable 的 notifyObservers()方法时,将调用观察者类中的update()方法。

  1. Observable类

java.util. Observable类充当观察目标类,在Observable中定义了一个向量Vector来存储观察者对象。它的方法包括:

方法名作用
Observable()构造函数,实例化Vector向量。
addObserver(Observer o)用于注册新的观察者对象到向量中。
deleteObserver (Observer o)用于删除向量中的某一个观察者对象。
notifyObservers( ) , notifyObservers(Object arg):通知方法,在方法内部循环调月向量中每一个观察者的update()方法
deleteObservers该方法用于清空向量,即删除向量中所有观察者对象
setChanged()该方法被调用后会将一个boolean类型的内部标记变量changed i值设置为true,表示观察目标对象的状态发生了变化。
clearChanged()该方法用于将changed变量的值设为false,表示对象状态不再发生改变或者已经通知了所有的观察者对象﹐调用了它们的update()方法。
hasChanged()该方法用于测试对象状态是否改变。
countObservers()该方法用于返回向量中观察者的数量。

我们可以直接使用Observer接口Observable类来作为观察者模式的抽象层,自定义具体的观察者类和观察目标类,通过使用Java API中的Observer接口和Observable类,可以更加方便地在Java语言中使用观察者模式。