行为型-观察者模式(Observer)

4/6/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 订阅报纸

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

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

# 二:解决方案

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

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

# 2.1 解决思路

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

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

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

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

# 2.2 模式结构和说明

classDiagram Subject "1" o--> "0..*" Observer Subject <|-- ConcreteSubject ConcreteSubject <.. ConcreteObserver Observer <|-- ConcreteObserver class Subject{ -List~Observer~ observer +attach(Observer) void +detach(Observer) void +notifyObservers() void } class Observer{ <<interface>> +update(Subject) void } class ConcreteSubject{ -String subjectState +getSubjectState() String +setSubjectState(String) void } class ConcreteObserver{ -String observerState +update(Subject) void }
  • Subject:目标对象,通常具有如下功能:
    1. 一个目标可以被多个观察者观察;
    2. 目标提供对观察者注册和退订的维护;
    3. 当目标的状态发生变化时,目标负责通知所有注册的、有效的观察者;
  • Observer:定义观察者的接口,提供目标通知时对应的更新方法,这个更新方法进行相应的业务处理,可以在这个方法里面回调目标对象,以获取目标对象的数据。
  • ConcreteSubject:具体的目标实现对象,用来维护目标状态,当目标对象的状态发生改变时,通知所有注册有效的观察者,让观察者执行相应的处理。
  • ConcreteObserver:观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理,比如更新自身的状态以保持和目标的相应状态一致。

# 2.3 示例代码

目标对象定义:

/**
 * 目标对象,它知道观察它的观察者,并提供注册和删除观察者的接口
 */
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

具体的目标对象:

/**
 * 具体的目标对象,负责把有关状态存入到相应的观察者对象,
 * 并在自己状态发生改变时,通知各个观察者
 */
public class ConcreteSubject extends Subject {
    /**
     * 示意,目标对象的状态
     */
    private String subjectState;

    public String getSubjectState() {
        return subjectState;
    }

    public void setSubjectState(String subjectState) {
        this.subjectState = subjectState;
        //状态发生了改变,通知各个观察者
        this.notifyObservers();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

观察者接口定义:

/**
 * 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象
 */
public interface Observer {

    /**
     * 更新的接口
     *
     * @param subject 传入目标对象,好获取相应的目标对象的状态
     */
    void update(Subject subject);
}
1
2
3
4
5
6
7
8
9
10
11

观察者具体实现:

/**
 * 具体观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
 */
public class ConcreteObserver implements Observer {
    
    /**
     * 示意,观者者的状态
     */
    private String observerState;

    public void update(Subject subject) {
        // 具体的更新实现
        //这里可能需要更新观察者的状态,使其与目标的状态保持一致
        observerState = ((ConcreteSubject) subject).getSubjectState();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.4 实现案例

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

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

classDiagram Subject "1" o--> "0..*" Observer Subject <|-- NewPaper NewPaper <.. Reader Observer <|-- Reader class Subject{ -List~Observer~ observer +attach(Observer) void +detach(Observer) void +notifyObservers() void } class Observer{ <<interface>> +update(Subject) void } class NewPaper{ -String content +getContent() String +setContent(String) void } class Reader{ -String name +update(Subject) void }

被观察的目标:

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

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

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

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

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

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

    /**
     * 报纸的读者需要先向报社订阅,先要注册
     *
     * @param reader 报纸的读者
     */
    public void attach(Observer reader) {
        readers.add(reader);
    }

    /**
     * 报纸的读者可以取消订阅
     *
     * @param reader 报纸的读者
     */
    public void detach(Observer reader) {
        readers.remove(reader);
    }

    /**
     * 当每期报纸印刷出来后,就要迅速主动的被送到读者的手中,
     * 相当于通知读者,让他们知道
     */
    protected void notifyObservers() {
        for (Observer reader : readers) {
            reader.update(this);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

报纸类实现:

/**
 * 报纸对象,具体的目标实现
 */
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

观察者:

/**
 * 观察者,比如报纸的读者
 */
public interface Observer {
    /**
     * 被通知的方法
     *
     * @param subject 具体的目标对象,可以获取报纸的内容
     */
    void update(Subject subject);
}
1
2
3
4
5
6
7
8
9
10
11

观察者实现:

/**
 * 真正的读者,为了简单就描述一下姓名
 */
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

客户端:

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("本期内容是观察者模式");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

运行结果:

# 三:模式讲解

# 3.1 认识观察者模式

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

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

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

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

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

2. 单向依赖

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

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

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

3. 基本的实现说明

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

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

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

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

4. 命名建议

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

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

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

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

5. 触发通知的时机

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

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

6. 相互观察

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

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

7. 通知的顺序

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

# 3.2 优缺点

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

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

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

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

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

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

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

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

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

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

# 3.3 思考观察者模式

1. 观察者模式的本质

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

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

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

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

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

2. 何时选用观察者模式

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

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

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

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

# 3.4 相关模式

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

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

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

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

# 四:JDK

# 五:参考文献

最后更新: 4/8/2022, 11:58:09 AM