【行为型模式十八】观察者模式(Observer)

1 场景问题#

1.1 订阅报纸的过程##

来考虑实际生活中订阅报纸的过程,这里简单总结了一下,订阅报纸的基本流程如下:

首先按照自己的需要选择合适的报纸,具体的报刊杂志目录可以从邮局获取;

选择好后,就到邮局去填写订阅单,同时交上所需的费用;

至此,就完成了报纸的订阅过程,接下去的就是耐心等候,报社会按照出报时间推出报纸,然后报纸会被送到每个订阅人的手里。

画个图来描述上述过程,如图所示:

订阅报纸的过程示意图

虽然看起来订阅者是直接跟邮局在打交道,但实际上,订阅者的订阅数据是会被邮局传递到报社的,当报社出版了报纸,报社会按照订阅信息把报纸交给邮局,然后由邮局来代为发送到订阅者的手中。所以在整个过程中,邮局只不过起到一个中转的作用,为了简单,我们去掉邮局,让订阅者直接和报社交互,如图所示:

简化的订阅报纸过程示意图

1.2 订阅报纸的问题##

在上述过程中,订阅者在完成订阅后,最关心的问题就是何时能收到新出的报纸。幸好在现实生活中,报纸都是定期出版,这样发放到订阅者手中也基本上有一个大致的时间范围,差不多到时间了,订阅者就会看看邮箱,查收新的报纸。

要是报纸出版的时间不固定呢?

那订阅者就麻烦了,如果订阅者想要第一时间阅读到新报纸,恐怕只能天天守着邮箱了,这未免也太痛苦了吧。

继续引申一下,用类来描述上述的过程,描述如下:

订阅者类向出版者类订阅报纸,很明显不会只有一个订阅者订阅报纸,订阅者类可以有很多;当出版者类出版新报纸的时候,多个订阅者类如何知道呢?还有订阅者类如何得到新报纸的内容呢?

把上面的问题对比描述一下:

进一步抽象描述这个问题:当一个对象的状态发生改变的时候,如何让依赖于它的所有对象得到通知,并进行相应的处理呢?

该如何解决这样的问题?

2 解决方案#

2.1 观察者模式来解决##

用来解决上述问题的一个合理的解决方案就是观察者模式。那么什么是观察者模式呢?

  1. 观察者模式定义
  1. 应用观察者模式来解决的思路

在前面描述的订阅报纸的例子里面,对于报社来说,在一开始,它并不清楚究竟有多少个订阅者会来订阅报纸,因此,报社需要维护一个订阅者的列表,这样当报社出版报纸的时候,才能够把报纸发放到所有的订阅者手中。对于订阅者来说,订阅者也就是看报的读者,多个订阅者会订阅同一份报纸。

这就出现了一个典型的一对多的对象关系,一个报纸对象,会有多个订阅者对象来订阅;当报纸出版的时候,也就是报纸对象改变的时候,需要通知所有的订阅者对象。那么怎么来建立并维护这样的关系呢?

观察者模式可以处理这种问题,观察者模式把这多个订阅者称为观察者:Observer,多个观察者观察的对象被称为目标:Subject。

一个目标可以有任意多个观察者对象,一旦目标的状态发生了改变,所有注册的观察者都会得到通知,然后各个观察者会对通知作出相应的响应,执行相应的业务功能处理,并使自己的状态和目标对象的状态保持一致。

2.2 模式结构和说明##

观察者模式结构如图所示:

观察者模式结构示意图

Subject:目标对象,通常具有如下功能:

     1. 一个目标可以被多个观察者观察;

     2. 目标提供对观察者注册和退订的维护;

     3. 当目标的状态发生变化时,目标负责通知所有注册的、有效的观察者;

Observer:定义观察者的接口,提供目标通知时对应的更新方法,这个更新方法进行相应的业务处理,可以在这个方法里面回调目标对象,以获取目标对象的数据。

ConcreteSubject:具体的目标实现对象,用来维护目标状态,当目标对象的状态发生改变时,通知所有注册有效的观察者,让观察者执行相应的处理。

ConcreteObserver:观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理,比如更新自身的状态以保持和目标的相应状态一致。

2.3 观察者模式示例代码##

  1. 先来看看目标对象的定义,示例代码如下:
/**
 * 目标对象,它知道观察它的观察者,并提供注册和删除观察者的接口
 */
public class Subject {
    /**
     * 用来保存注册的观察者对象
     */
    private List<Observer> observers = new ArrayList<Observer>();
    /**
     * 注册观察者对象
     * @param observer 观察者对象
     */
    public void attach(Observer observer) {
       observers.add(observer);
    }
    /**
     * 删除观察者对象
     * @param observer 观察者对象
     */
    public void detach(Observer observer) {
       observers.remove(observer);
    }
    /**
     * 通知所有注册的观察者对象
     */
    protected void notifyObservers() {
       for(Observer observer : observers){
           observer.update(this);
       }
    }
}
  1. 接下来看看具体的目标对象,示例代码如下:
/**
 * 具体的目标对象,负责把有关状态存入到相应的观察者对象,
 * 并在自己状态发生改变时,通知各个观察者
 */
public class ConcreteSubject extends Subject {
    /**
     * 示意,目标对象的状态
     */
    private String subjectState;
    public String getSubjectState() {
       return subjectState;
    }
    public void setSubjectState(String subjectState) {
       this.subjectState = subjectState;
       //状态发生了改变,通知各个观察者
       this.notifyObservers();
    }
}
  1. 再来看看观察者的接口定义,示例代码如下:
/**
 * 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象
 */
public interface Observer {
    /**
     * 更新的接口
     * @param subject 传入目标对象,好获取相应的目标对象的状态
     */
    public void update(Subject subject);
}
  1. 接下来看看观察者的具体实现示意,示例代码如下:
/**
 * 具体观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
 */
public class ConcreteObserver implements Observer {
    /**
     * 示意,观者者的状态
     */
    private String observerState;
  
    public void update(Subject subject) {
       // 具体的更新实现
       //这里可能需要更新观察者的状态,使其与目标的状态保持一致
       observerState = ((ConcreteSubject)subject).getSubjectState();
    }
}

2.4 使用观察者模式实现示例##

要使用观察者模式来实现示例,那就按照前面讲述的实现思路,把报纸对象当作目标,然后订阅者当做观察者,就可以实现出来了。

使用观察者模式来实现示例的结构如图所示:

使用观察者模式来实现示例的结构示意图
  1. 被观察的目标

在前面描述的订阅报纸的例子里面,多个订阅者都是在观察同一个报社对象,这个报社对象就是被观察的目标。这个目标的接口应该有些什么方法呢?还是从实际入手去想,看看报社都有些什么功能。报社最基本有如下的功能:

注册订阅者,也就是说很多个人来订报纸,报社肯定要有相应的记录才行;

出版报纸,这个是报社的主要工作;

发行报纸,也就是要把出版的报纸发送到订阅者手中;

退订报纸,当订阅者不想要继续订阅了,可以取消订阅;

上面这些功能是报社最最基本的功能,当然,报社还有很多别的功能,为了简单起见,这里就不再去描述了。因此报社这个目标的接口也应该实现上述功能,把他们定义在目标接口里面,示例代码如下:

/**
 * 目标对象,作为被观察者
 */
public class Subject {
    /**
     * 用来保存注册的观察者对象,也就是报纸的订阅者
     */
    private List<Observer> readers = new ArrayList<Observer>();

    /**
     * 报纸的读者需要先向报社订阅,先要注册
     * @param reader 报纸的读者
     * @return 是否注册成功
     */
    public void attach(Observer reader) {
       readers.add(reader);
    }
    /**
     * 报纸的读者可以取消订阅
     * @param reader 报纸的读者
     * @return 是否取消成功
     */
    public void detach(Observer reader) {
       readers.remove(reader);
    }
    /**
     * 当每期报纸印刷出来后,就要迅速主动的被送到读者的手中,
     * 相当于通知读者,让他们知道
     */
    protected void notifyObservers() {
       for(Observer reader : readers){
           reader.update(this);
       }
    }
}

细心的朋友可能会发现,这个对象并没有定义出版报纸的功能,这是为了让这个对象更加通用,这个功能还是有的,放到具体报纸类里面去了,下面就来具体的看看具体的报纸类的实现。

为了演示简单,在这个实现类里面增添一个属性,用它来保存报纸的内容,然后增添一个方法来修改这个属性,修改这个属性就相当于出版了新的报纸,并且同时通知所有的订阅者。示例代码如下:

/**
 * 报纸对象,具体的目标实现
 */
public class NewsPaper extends Subject{
    /**
     * 报纸的具体内容
     */
    private String content;
    /**
     * 获取报纸的具体内容
     * @return 报纸的具体内容
     */
    public String getContent() {
       return content;
    }

    /**
     * 示意,设置报纸的具体内容,相当于要出版报纸了
     * @param content 报纸的具体内容
     */
    public void setContent(String content) {
       this.content = content;
       //内容有了,说明又出报纸了,那就通知所有的读者
       notifyObservers();
    }
}
  1. 观察者

目标定义好过后,接下来把观察者抽象出来,看看它应该具有什么功能。分析前面的描述,发现观察者只要去邮局注册了过后,就是等着接收报纸就好了,没有什么其它的功能。那么就把这个接收报纸的功能抽象成为更新的方法,从而定义出观察者接口来,示例代码如下:

/**
 * 观察者,比如报纸的读者
 */
public interface Observer {
    /**
     * 被通知的方法
     * @param subject 具体的目标对象,可以获取报纸的内容
     */
    public void update(Subject subject);
}

定义好了观察者的接口过后,该来想想如何实现了。具体的观察者需要实现:在收到被通知的内容后,自身如何进行相应处理的功能。为了演示的简单,收到报纸内容过后,简单的输出一下,表示收到了就行了。

定义一个简单的观察者实现,示例代码如下:

/**
 * 真正的读者,为了简单就描述一下姓名
 */
public class Reader implements Observer{
    /**
     * 读者的姓名
     */
    private String name;

    public void update(Subject subject) {
       //这是采用拉的方式
       System.out.println(name+"收到报纸了,阅读先。内容是==="+((NewsPaper)subject).getContent());
    }

    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
}
  1. 使用观察者模式

前面定义好了观察者和观察的目标,那么如何使用它们呢?

那就写个客户端,在客户端里面,先创建好一个报纸,作为被观察的目标,然后多创建几个读者作为观察者,当然需要把这些观察者都注册到目标里面去,接下来就可以出版报纸了,具体的示例代码如下:

public class Client {
    public static void main(String[] args) {
       //创建一个报纸,作为被观察者
       NewsPaper subject = new NewsPaper();
       //创建阅读者,也就是观察者
       Reader reader1 = new Reader();
       reader1.setName("张三");
     
       Reader reader2 = new Reader();
       reader2.setName("李四");
     
       Reader reader3 = new Reader();
       reader3.setName("王五");
     
       //注册阅读者
       subject.attach(reader1);
       subject.attach(reader2);
       subject.attach(reader3);
     
       //要出报纸啦
       subject.setContent("本期内容是观察者模式");
    }
}

运行结果如下:

张三收到报纸了,阅读先。内容是===本期内容是观察者模式
李四收到报纸了,阅读先。内容是===本期内容是观察者模式
王五收到报纸了,阅读先。内容是===本期内容是观察者模式

你还可以通过改变注册的观察者,或者是注册了又退订,来看看输出的结果。会发现没有注册或者退订的观察者是收不到报纸的。

如同前面的示例,读者和报社是一种典型的一对多的关系,一个报社有多个读者,当报社的状态发生改变,也就是出版新报纸的时候,所有注册的读者都会得到通知,然后读者会拿到报纸,读者会去阅读报纸并进行后续的操作。

3 模式讲解#

3.1 认识观察者模式##

  1. 目标和观察者之间的关系

按照模式的定义,目标和观察者之间是典型的一对多的关系。

但是要注意,如果观察者只有一个,也是可以的,这样就变相实现了目标和观察者之间一对一的关系,这也使得在处理一个对象的状态变化会影响到另一个对象的时候,也可以考虑使用观察者模式。

同样的,一个观察者也可以观察多个目标,如果观察者为多个目标定义的通知更新方法都是update方法的话,这会带来麻烦,因为需要接收多个目标的通知,如果是一个update的方法,那就需要在方法内部区分,到底这个更新的通知来自于哪一个目标,不同的目标有不同的后续操作

一般情况下,观察者应该为不同的观察者目标,定义不同的回调方法,这样实现最简单,不需要在update方法内部进行区分。

  1. 单向依赖

在观察者模式中,观察者和目标是单向依赖的,只有观察者依赖于目标,而目标是不会依赖于观察者的

它们之间联系的主动权掌握在目标手中,只有目标知道什么时候需要通知观察者,在整个过程中,观察者始终是被动的,被动的等待目标的通知,等待目标传值给它。

对目标而言,所有的观察者都是一样的,目标会一视同仁的对待。当然也可以通过在目标里面进行控制,实现有区别对待观察者,比如某些状态变化,只需要通知部分观察者,但那是属于稍微变形的用法了,不属于标准的、原始的观察者模式了。

  1. 基本的实现说明

具体的目标实现对象要能维护观察者的注册信息,最简单的实现方案就如同前面的例子那样,采用一个集合来保存观察者的注册信息。

具体的目标实现对象需要维护引起通知的状态,一般情况下是目标自身的状态,变形使用的情况下,也可以是别的对象的状态。

具体的观察者实现对象需要能接收目标的通知,能够接收目标传递的数据,或者是能够主动去获取目标的数据,并进行后续处理。

如果是一个观察者观察多个目标,那么在观察者的更新方法里面,需要去判断是来自哪一个目标的通知。一种简单的解决方案就是扩展update方法,比如在方法里面多传递一个参数进行区分等;还有一种更简单的方法,那就是干脆定义不同的回调方法。

  1. 命名建议

观察者模式又被称为发布-订阅模式;

目标接口的定义,建议在名称后面跟Subject;

观察者接口的定义,建议在名称后面跟Observer;

观察者接口的更新方法,建议名称为update,当然方法的参数可以根据需要定义,参数个数不限、参数类型不限;

  1. 触发通知的时机

在实现观察者模式的时候,一定要注意触发通知的时机,一般情况下,是在完成了状态维护后触发,因为通知会传递数据,不能够先通知后改数据,这很容易出问题,会导致观察者和目标对象的状态不一致。比如:目标一发出通知,就有观察者来取值,结果目标还没有更新数据,这就明显造成了错误。如下示例就是有问题的了,示例代码如下:

public void setContent(String content) {
    //一激动,目标先发出通知了,然后才修改自己的数据,这会造成问题
    notifyAllReader();  
    this.content = content;
}
  1. 相互观察

在某些应用里面,可能会出现目标和观察者相互观察的情况。什么意思呢,比如有两套观察者模式的应用,其中一套观察者模式的实现是A对象、B对象观察C对象;在另一套观察者模式的实现里面,实现的是B对象、C对象观察A对象,那么A对象和C对象就是在相互观察。

换句话说,A对象的状态变化会引起C对象的联动操作,反过来,C 对象的状态变化也会引起A对象的联动操作。对于出现这种状况,要特别小心处理,因为可能会出现死循环的情况。

  1. 观察者模式的调用顺序示意图

在使用观察者模式时,会很明显的分成两个阶段,第一个阶段是准备阶段,也就是维护目标和观察者关系的阶段,这个阶段的调用顺序如图所示:

观察者模式准备阶段示意图

接下来就是实际的运行阶段了,这个阶段的调用顺序如图所示:

观察者模式运行阶段示意图
  1. 通知的顺序

从理论上说,当目标对象的状态变化后通知所有观察者的时候,顺序是不确定的,因此观察者实现的功能,绝对不要依赖于通知的顺序,也就是说,多个观察者之间的功能是平行的,相互不应该有先后的依赖关系

3.2 推模型和拉模型##

在观察者模式的实现里面,又分为推模型和拉模型两种方式,什么意思呢?

  1. 推模型

目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据,相当于是在广播通信。

  1. 拉模型

目标对象在通知观察者的时候,只传递少量信息,如果观察者需要更具体的信息,由观察者主动到目标对象中获取,相当于是观察者从目标对象中拉数据。

一般这种模型的实现中,会把目标对象自身通过update方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

根据上面的描述,发现前面的例子就是典型的拉模型,那么推模型如何实现呢,还是来看个示例吧,这样会比较清楚。

  1. 推模型的观察者接口

根据前面的讲述,推模型通常都是把需要传递的数据直接推送给观察者对象,所以观察者接口中的update方法的参数需要发生变化,示例代码如下:

/**
 * 观察者,比如报纸的读者
 */
public interface Observer {
    /**
     * 被通知的方法,直接把报纸的内容推送过来
     * @param content 报纸的内容
     */
    public void update(String content);
}
  1. 推模型的观察者的具体实现

以前需要到目标对象里面获取自己需要的数据,现在是直接接收传入的数据,这就是改变的地方,示例代码如下:

public class Reader implements Observer{
    /**
     * 读者的姓名
     */
    private String name;

    public void update(String content) {
       //这是采用推的方式
       System.out.println(name+"收到报纸了,阅读先。内容是==="+content);
    }
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
}
  1. 推模型的目标对象

跟拉模型的目标实现相比,有一些变化:

一个就是通知所有观察者的方法,以前是没有参数的,现在需要传入需要主动推送的数据;

另外一个就是在循环通知观察者的时候,也就是循环调用观察者的update方法的时候,传入的参数不同了;

示例代码如下:

/**
 * 目标对象,作为被观察者,使用推模型
 */
public class Subject {
    /**
     * 用来保存注册的观察者对象,也就是报纸的订阅者
     */
    private List<Observer> readers = new ArrayList<Observer>();
    /**
     * 报纸的读者需要先向报社订阅,先要注册
     * @param reader 报纸的读者
     * @return 是否注册成功
     */
    public void attach(Observer reader) {
       readers.add(reader);
    }
    /**
     * 报纸的读者可以取消订阅
     * @param reader 报纸的读者
     * @return 是否取消成功
     */
    public void detach(Observer reader) {
       readers.remove(reader);
    }
    /**
     * 当每期报纸印刷出来后,就要迅速的主动的被送到读者的手中,
     * 相当于通知读者,让他们知道
     * @param content 要主动推送的内容
     */
    protected void notifyObservers(String content) {
       for(Observer reader : readers){
           reader.update(content);
       }
    }
}
  1. 推模型的目标具体实现

跟拉模型相比,有一点变化,就是在调用通知观察者的方法的时候,需要传入参数了,拉模型的实现中是不需要的,示例代码如下:

public class NewsPaper extends Subject{
    private String content;
    public String getContent() {
       return content;
    }
    public void setContent(String content) {
       this.content = content;
       //内容有了,说明又出报纸了,那就通知所有的读者
       notifyObservers(content);
    }
}
  1. 推模型的客户端使用

跟拉模型一样,没有变化。

  1. 关于两种模型的比较

两种实现模型,在开发的时候,究竟应该使用哪一种,还是应该具体问题具体分析。这里,只是把两种模型进行一个简单的比较。

推模型是假定目标对象知道观察者需要的数据;而拉模型是目标对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传给观察者,让观察者自己去按需取值。

推模型可能会使得观察者对象难以复用,因为观察者定义的update方法是按需而定义的,可能无法兼顾没有考虑到的使用情况。这就意味着出现新情况的时候,就可能需要提供新的update方法,或者是干脆重新实现观察者。

而拉模型就不会造成这样的情况,因为拉模型下,update方法的参数是目标对象本身,这基本上是目标对象能传递的最大数据集合了,基本上可以适应各种情况的需要。

3.3 Java中的观察者模式##

估计有些朋友在看前面的内容的时候,心里就嘀咕上了,Java里面不是已经有了观察者模式的部分实现吗,为何还要全部自己从头做呢?

主要是为了让大家更好的理解观察者模式本身,而不用受Java语言实现的限制。

好了,下面就来看看如何利用Java中已有的功能来实现观察者模式。在java.util包里面有一个类Observable,它实现了大部分我们需要的目标的功能还有一个接口Observer,它里面定义了update的方法,就是观察者的接口

因此,利用Java中已有的功能来实现观察者模式非常简单,跟前面完全由自己来实现观察者模式相比有如下改变:

不需要再定义观察者和目标的接口了,JDK帮忙定义了;

具体的目标实现里面不需要再维护观察者的注册信息了,这个在Java中的Observable类里面,已经帮忙实现好了;

触发通知的方式有一点变化,要先调用setChanged方法,这个是Java为了帮助实现更精确的触发控制而提供的功能;

具体观察者的实现里面,update方法其实能同时支持推模型和拉模型,这个是Java在定义的时候,就已经考虑进去了;

好了,说了这么多,还是看看例子会比较直观。

  1. 新的目标的实现,不再需要自己来实现Subject定义,在具体实现的时候,也不是继承Subject了,而是改成继承Java中定义的Observable,示例代码如下:
/**
 * 报纸对象,具体的目标实现
 */
public class NewsPaper extends  java.util.Observable   {
    /**
     * 报纸的具体内容
     */
    private String content;
    /**
     * 获取报纸的具体内容
     * @return 报纸的具体内容
     */
    public String getContent() {
       return content;
    }
    /**
     * 示意,设置报纸的具体内容,相当于要出版报纸了
     * @param content 报纸的具体内容
     */
    public void setContent(String content) {
       this.content = content;
       //内容有了,说明又出新报纸了,那就通知所有的读者
       //注意在用Java中的Observer模式的时候,下面这句话不可少
       this.setChanged();
       //然后主动通知,这里用的是推的方式
       this.notifyObservers(this.content);
       //如果用拉的方式,这么调用
       //this.notifyObservers();
    }
}
  1. 再看看新的观察者的实现,不是实现自己定义的观察者接口,而是实现由Java提供的Observer接口,示例代码如下:
/**
 * 真正的读者,为了简单就描述一下姓名
 */
public class Reader implements   java.util.Observer   {
    /**
     * 读者的姓名
     */
    private String name;
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
    public void update(Observable o, Object obj) {
       //这是采用推的方式
       System.out.println(name+"收到报纸了,阅读先。目标推过来的内容是==="+obj);

       //这是获取拉的数据
       System.out.println(name+"收到报纸了,阅读先。主动到目标对象去拉的内容是==="+((NewsPaper)o).getContent());
    }
}
  1. 客户端使用

客户端跟前面的写法没有太大改变,主要在注册阅读者的时候,调用的方法跟以前不一样了,示例代码如下:

public class Client {
    public static void main(String[] args) {
       //创建一个报纸,作为被观察者
       NewsPaper subject = new NewsPaper();
       //创建阅读者,也就是观察者
       Reader reader1 = new Reader();
       reader1.setName("张三");
     
       Reader reader2 = new Reader();
       reader2.setName("李四");
     
       Reader reader3 = new Reader();
       reader3.setName("王五");
     
       //注册阅读者
       subject.addObserver(reader1);
       subject.addObserver(reader2);
       subject.addObserver(reader3);
     
       //要出报纸啦
       subject.setContent("本期内容是观察者模式");
    }
}

赶紧测试一下,运行运行,看看结果,运行结果如下所示:

王五收到报纸了,阅读先。目标推过来的内容是===本期内容是观察者模式
王五收到报纸了,阅读先。主动到目标对象去拉的内容是===本期内容是观察者模式
李四收到报纸了,阅读先。目标推过来的内容是===本期内容是观察者模式
李四收到报纸了,阅读先。主动到目标对象去拉的内容是===本期内容是观察者模式
张三收到报纸了,阅读先。目标推过来的内容是===本期内容是观察者模式
张三收到报纸了,阅读先。主动到目标对象去拉的内容是===本期内容是观察者模式

然后好好对比自己实现观察者模式和使用Java已有的功能来实现观察者模式,看看有什么不同,有什么相同,好好体会一下。

3.4 观察者模式的优缺点##

  1. 观察者模式实现了观察者和目标之间的抽象耦合

原本目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口过后,目标和观察者就只是在抽象层面上耦合了,也就是说目标只是知道观察者接口,并不知道具体的观察者的类,从而实现目标类和具体的观察者类之间解耦。

  1. 观察者模式实现了动态联动

所谓联动,就是做一个操作会引起其它相关的操作。由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态的控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动。

  1. 观察者模式支持广播通信

由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行广播。当然,也可以通过在目标上添加新的功能来限制广播的范围。

在广播通信的时候要注意一个问题,就是相互广播造成死循环的问题。比如A和B两个对象互为观察者和目标对象,A对象发生状态变化,然后A来广播信息,B对象接收到通知后,在处理过程中,使得B对象的状态也发生了改变,然后B来广播信息,然后A对象接到通知后,又触发广播信息……,如此A引起B变化,B又引起A变化,从而一直相互广播信息,就造成死循环了。

  1. 观察者模式可能会引起无谓的操作

由于观察者模式每次都是广播通信,不管观察者需不需要,每个观察者都会被调用update方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。

其实浪费了还好,怕就怕引起了误更新,那就麻烦了,比如:本应该在执行这次状态更新前把某个观察者删除掉,这样通知的时候就没有这个观察者了,但是现在忘掉了,那么就会引起误操作。

3.5 思考观察者模式##

  1. 观察者模式的本质

观察者模式的本质:触发联动。

当修改目标对象的状态的时候,就会触发相应的通知,然后会循环调用所有注册的观察者对象的相应方法,其实就相当于联动调用这些观察者的方法。

而且这个联动还是动态的,可以通过注册和取消注册来控制观察者,因而可以在程序运行期间,通过动态的控制观察者,来变相的实现添加和删除某些功能处理,这些功能就是观察者在update的时候执行的功能。

同时目标对象和观察者对象的解耦,又保证了无论观察者发生怎样的变化,目标对象总是能够正确地联动过来。

理解这个本质对我们非常有用,对于我们识别和使用观察者模式有非常重要的意义,尤其是在变形使用的时候,万变不离其宗。

  1. 何时选用观察者模式

建议在如下情况中,选用观察者模式:

当一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态变化,那么就可以选用观察者模式,将这两者封装成观察者和目标对象,当目标对象变化的时候,依赖于它的观察者对象也会发生相应的变化。这样就把抽象模型的这两个方面分离开了,使得它们可以独立的改变和复用。

如果在更改一个对象的时候,需要同时连带改变其它的对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式,被更改的那一个对象很明显就相当于是目标对象,而需要连带修改的多个其它对象,就作为多个观察者对象了。

当一个对象必须通知其它的对象,但是你又希望这个对象和其它被它通知的对象是松散耦合的,也就是说这个对象其实不想知道具体被通知的对象,这种情况可以选用观察者模式,这个对象就相当于是目标对象,而被它通知的对象就是观察者对象了。

3.6 Swing中的观察者模式##

Java的Swing中到处都是观察者模式的身影,比如大家熟悉的事件处理,就是典型的观察者模式的应用。(说明一下:早期的Swing事件处理用的是职责链)

Swing组件是被观察的目标,而每个实现监听器的类就是观察者,监听器的接口就是观察者的接口,在调用addXXXListener方法的时候就相当于注册观察者。当组件被点击,状态发生改变的时候,就会产生相应的通知,会调用注册的观察者的方法,就是我们所实现的监听器的方法。

从这里还可以学一招:如何处理一个观察者观察多个目标对象?

你看一个Swing的应用程序,作为一个观察者,经常会注册观察多个不同的目标对象,也就是同一类,既实现了按钮组件的事件处理,又实现了文本框组件的事件处理,是怎么做到的呢?

答案就在监听器接口上,这些监听器接口就相当于观察者接口,也就是说一个观察者要观察多个目标对象,只要不同的目标对象使用不同的观察者接口就好了,当然,这些接口里面的方法也不相同,不再都是update方法了。这样一来,不同的目标对象通知观察者所调用的方法也就不同了,这样在具体实现观察者的时候,也就实现成不同的方法,自然就区分开了。

3.7 简单变形示例——区别对待观察者##

首先声明,这里只是举一个非常简单的变形使用的例子,也可算是基本的观察者模式的功能加强,事实上可以有很多很多的变形应用,这也是为什么我们特别强调大家要深入理解每个设计模式,要把握每个模式的本质的原因了。

  1. 范例需求

这是一个实际系统的简化需求:在一个水质监测系统中有这样一个功能,当水中的杂质为正常的时候,只是通知监测人员做记录;当为轻度污染的时候,除了通知监测人员做记录外,还要通知预警人员,判断是否需要预警;当为中度或者高度污染的时候,除了通知监测人员做记录外,还要通知预警人员,判断是否需要预警,同时还要通知监测部门领导做相应的处理。

  1. 解决思路和范例代码

分析上述需求就会发现,对于水质污染这件事情,有可能会涉及到监测员、预警人员、监测部门领导,根据不同的水质污染情况涉及到不同的人员,也就是说,监测员、预警人员、监测部门领导他们三者是平行的,职责都是处理水质污染,但是处理的范围不一样。

因此很容易套用上观察者模式,如果把水质污染的记录当作被观察的目标的话,那么监测员、预警人员和监测部门领导就都是观察者了。

前面学过的观察者模式,当目标通知观察者的时候是全部都通知,但是现在这个需求是不同的情况来让不同的人处理,怎么办呢?

解决的方式通常有两种,一种是目标可以通知,但是观察者不做任何操作;另外一种是在目标里面进行判断,干脆就不通知了。两种实现方式各有千秋,这里选择后面一种方式来示例,这种方式能够统一逻辑控制,并进行观察者的统一分派,有利于业务控制和今后的扩展。

(1)先来定义观察者的接口,这个接口跟前面的示例差别也不大,只是新加了访问观察人员职务的方法,示例代码如下:

/**
 * 水质观察者接口定义
 */
public interface WatcherObserver {
    /**
     * 被通知的方法
     * @param subject 传入被观察的目标对象
     */
    public void update(WaterQualitySubject subject);
    /**
     * 设置观察人员的职务
     * @param job 观察人员的职务
     */
    public void setJob(String job);
    /**
     * 获取观察人员的职务
     * @return 观察人员的职务
     */
    public String getJob();
}

(2)定义完接口后,来看看观察者的具体实现,示例代码如下:

/**
 * 具体的观察者实现
 */
public class Watcher implements WatcherObserver{
    /**
     * 职务
     */
    private String job;
    public String getJob() {
       return this.job;
    }  
    public void setJob(String job) {
       this.job = job;
    }

    public void update(WaterQualitySubject subject) {
       //这里采用的是拉的方式
       System.out.println(job+"获取到通知,当前污染级别为:"+subject.getPolluteLevel());
    }
}

(3)接下来定义目标的父对象,跟以前相比有些改变:

把父类实现成抽象的,因为在里面要定义抽象的方法;

原来通知所有的观察者的方法被去掉了,这个方法现在需要由子类去实现,要按照业务来有区别的来对待观察者,得看看是否需要通知观察者;

新添加一个水质污染级别的业务方法,这样在观察者获取目标对象的数据的时候,就不需要再知道具体的目标对象,也不需要强制造型了;

/**
 * 定义水质监测的目标对象
 */
public abstract class WaterQualitySubject {
    /**
     * 用来保存注册的观察者对象
     */
    protected List<WatcherObserver> observers = new ArrayList<WatcherObserver>();
    /**
     * 注册观察者对象
     * @param observer 观察者对象
     */
    public void attach(WatcherObserver observer) {
       observers.add(observer);
    }
    /**
     * 删除观察者对象
     * @param observer 观察者对象
     */
    public void detach(WatcherObserver observer) {
       observers.remove(observer);
    }
    /**
     * 通知相应的观察者对象
     */
    public abstract void notifyWatchers();
    /**
     * 获取水质污染的级别
     * @return 水质污染的级别
     */
    public abstract int getPolluteLevel();
}

(4)接下来重点看看目标的实现,在目标对象里面,添加一个描述污染级别的属性,在判断是否需要通知观察者的时候,不同的污染程度对应会通知不同的观察者,示例代码如下:

/**
 * 具体的水质监测对象
 */
public class WaterQuality extends WaterQualitySubject{
    /**
     * 污染的级别,0表示正常,1表示轻度污染,2表示中度污染,3表示高度污染
     */
    private int polluteLevel = 0;
    /**
     * 获取水质污染的级别
     * @return 水质污染的级别
     */
    public int getPolluteLevel() {
       return polluteLevel;
    }
    /**
     * 当监测水质情况后,设置水质污染的级别
     * @param polluteLevel 水质污染的级别
     */
    public void setPolluteLevel(int polluteLevel) {
       this.polluteLevel = polluteLevel;
       //通知相应的观察者
       this.notifyWatchers();
    }
    /**
     * 通知相应的观察者对象
     */
    public void notifyWatchers() {
       //循环所有注册的观察者
       for(WatcherObserver watcher : observers){
           //开始根据污染级别判断是否需要通知,由这里总控
           if(this.polluteLevel >= 0){
               //通知监测员做记录
               if("监测人员".equals(watcher.getJob())){
                   watcher.update(this);
               }
           }
           if(this.polluteLevel >= 1){
               //通知预警人员
               if("预警人员".equals(watcher.getJob())){
                   watcher.update(this);
               }
           }
           if(this.polluteLevel >= 2){
               //通知监测部门领导
               if("监测部门领导".equals(watcher.getJob())){
                   watcher.update(this);
               }
           }
       }
    }
}

(5)大功告成,来写个客户端,测试一下,示例代码如下:

public class Client {
    public static void main(String[] args) {
       //创建水质主题对象
       WaterQuality subject = new WaterQuality();
       //创建几个观察者
       WatcherObserver watcher1 = new Watcher();
       watcher1.setJob("监测人员");
       WatcherObserver watcher2 = new Watcher();
       watcher2.setJob("预警人员");
       WatcherObserver watcher3 = new Watcher();
       watcher3.setJob("监测部门领导");

       //注册观察者
       subject.attach(watcher1);
       subject.attach(watcher2);
       subject.attach(watcher3);
     
       //填写水质报告
       System.out.println("当水质为正常的时候------------------〉");
       subject.setPolluteLevel(0);
       System.out.println("当水质为轻度污染的时候---------------〉");
       subject.setPolluteLevel(1);
       System.out.println("当水质为中度污染的时候---------------〉");
       subject.setPolluteLevel(2);
    }
}

(6)运行一下,看看结果,如下:

当水质为正常的时候------------------〉
监测人员获取到通知,当前污染级别为:0
当水质为轻度污染的时候---------------〉
监测人员获取到通知,当前污染级别为:1
预警人员获取到通知,当前污染级别为:1
当水质为中度污染的时候---------------〉
监测人员获取到通知,当前污染级别为:2
预警人员获取到通知,当前污染级别为:2
监测部门领导获取到通知,当前污染级别为:2

仔细观察上面输出的结果,你会发现,当填写不同的污染级别时,被通知的人员是不同的。但是这些观察者是不知道这些不同的,观察者只是在自己获得通知的时候去执行自己的工作。具体要不要通知,什么时候通知都是目标对象的工作

3.8 相关模式##

  1. 观察者模式和状态模式

观察者模式和状态模式是有相似之处的。

观察者模式是当目标状态发生改变时,触发并通知观察者,让观察者去执行相应的操作。而状态模式是根据不同的状态,选择不同的实现,这个实现类的主要功能就是针对状态的相应的操作,它不像观察者,观察者本身还有很多其它的功能,接收通知并执行相应处理只是观察者的部分功能。

当然观察者模式和状态模式是可以结合使用的。观察者模式的重心在触发联动,但是到底决定哪些观察者会被联动,这时就可以采用状态模式来实现了,也可以采用策略模式来进行选择需要联动的观察者

  1. 观察者模式和中介者模式

观察者模式和中介者模式是可以结合使用的。

前面的例子中目标都只是简单的通知一下,然后让各个观察者自己去完成更新就结束了。如果观察者和被观察的目标之间的交互关系很复杂,比如:有一个界面,里面有三个下拉列表组件,分别是选择国家、省份/州、具体的城市,很明显这是一个三级联动,当你选择一个国家的时候,省份/州应该相应改变数据,省份/州一改变,具体的城市也需要改变。

这种情况下,很明显需要相关的状态都联动准备好了,然后再一次性的通知观察者,就是界面做更新处理,不会国家改变一下,省份和城市还没有改,就通知界面更新。这种情况就可以使用中介者模式来封装观察者和目标的关系。

在使用Swing的小型应用里面,也可以使用中介者模式。比如:把一个界面所有的事件用一个对象来处理,把一个组件触发事件过后,需要操作其它组件的动作都封装到一起,这个对象就是典型的中介者。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容