观察者模式

观察者模式

  • by Head First 设计模式

    在对象之间建立一对多的依赖,这样一来,当一个对象的状态改变,依赖它的对象都会收到通知,并且自动更新。

  • by Dive into Design Patterns:

    Also Known as: Event-Subscriber, Listener

    Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.

设计原则 #

  1. 找出应用之中可以变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
  2. 针对接口编程,而不是针对实现编程
  3. 多用组合,少用继承
  4. 为交互对象的松耦合设计而努力
    • 事实上,不用设计模式也可以硬编码出发布者-订阅者工作模式的代码,只不过发布者与订阅者呆在一起,会比较臃肿😮, 也不利于扩展。
    • 在观察者模式中,被观察者(发布者)与观察者(订阅者)是松耦合的,发布者并不关心订阅者的具体细节,只需要知道 其订阅与否,就知道状态变化后是否对其发送通知;同样地,订阅者也不关心发布者如何通知它,只需要处理好自己收到 通知的业务就行了😊
    • 松耦合的设计优势得以体现:代码有层次感,易于拓展和维护。

想想看MVC开发模式,这是不是松耦合的设计呢?控制层、模型层、视图层分别有自己的业务范围

UML简图 #

classDiagram
direction LR
class Publisher {
  << interface >>
  + registSubscriber(Subscriber s)
  + unregistSubscriber(Subscriber s)
  + notifySubscribers()
}

Publisher <|.. Client
Client *..> Subscriber
class Client {
  - List~Subscriber~ subscribers
  - Boolean state
  + registSubscriber(Subscriber s)
  + unregistSubscriber(Subscriber s)
  + notifySubscribers()
}
class Subscriber {
  << interface >>
  +update()
}
Subscriber <|.. ConcreteSubscriber : impl
class ConcreteSubscriber {
  ...
  + update()
}

笔记 #

  1. 观察者模式定义了对象之间一对多的关系。
  2. 发布者(被观察者)用一个统一的接口来更新观察者。
  3. 发布者和订阅者之间使用松耦合loose-coupling)的方式结合,订阅者不知道观察者的细节,只知道观察者实现观察者接口。
  4. 使用此模式时,订阅者可以从发布者处"推"或者"拉"数据, 不过"推"一般被认为是正确的方式。
  5. 有多个订阅者时,可以不依赖特性的通知顺序。
  6. Java提供了此模式的包,包括java.util.ObservableDeprecated since Java 9)。
  7. 此模式被用在其他地方,如JavaBeans,RMI。

示例代码 #

发布者 #

发布者(Publisher)是一个接口,主要定义了三个方法:

1void register(Subscriber) // 添加订阅
2void unregister(Subscriber) //取消订阅
3void notify() // 发布消息

除了以上的方法外,发布者自然可以添加其他的方法,根据具体业务需求。不过上述3个方法是必须的。

以下是发布者的示例代码,以下示例没有使用所谓的PublisherSubscriber名字,希望你能不通过名字,也能认出它们。

发布者被定义为Subject,意即主题,正所谓先有”主题“,才可以”订阅“。

1public interface Subject {
2
3    void registerBoard(Board board);
4    void unregisterBoard(Board board);
5    void notifyBoard();
6
7    // other businesses
8}

WeatherStation实现了发布者Subject接口,它定义了一个天气站,用来储存温度、湿度、压力等等天气信息。

 1public class WeatherStation implements Subject {
 2
 3    // 并发风险
 4    private List<Board> boards;
 5    private boolean status;
 6
 7    public WeatherStation() {
 8        this.boards = new LinkedList<>();
 9        this.status = false;
10    }
11
12    @Override
13    public void registerBoard(Board board) {
14        if (!boards.contains(board)) {
15            boards.add(board);
16        }
17    }
18
19    @Override
20    public void unregisterBoard(Board board) {
21        boards.remove(board);
22    }
23
24    @Override
25    public void notifyBoard() {
26        if (status) {
27            for (Board board : boards) {
28                board.update(this);
29            }
30            status = false;
31        }
32    }
33
34    public void setStatus(boolean status) {
35        this.status = status;
36    }
37
38    private float temperature;
39    private float humidity;
40    private float pressure;
41
42    public void setData(float temperature,
43                        float humidity,
44                        float pressure) {
45        this.temperature = temperature;
46        this.humidity = humidity;
47        this.pressure = pressure;
48        this.status = true;
49        //    notifyBoard(); // 可以在此处发出通知
50    }
51
52    public float getTemperature() {
53        return temperature;
54    }
55
56    public float getHumidity() {
57        return humidity;
58    }
59
60    public float getPressure() {
61        return pressure;
62    }
63}

订阅者 #

订阅者(Subscriber)也是一个接口,通常,它只有一个方法,用来更新信息:

1public interface Board {
2  // 观察者收到通知之后的更新,方法参数可以是指定字段或者实体
3  void update(WeatherStation client);
4}

订阅者的实现,就比较简单了。在发布-订阅模型里,订阅者的方法总是发布者主动调用的。

 1public class StatisticsBoard implements Board {
 2    @Override
 3    public void update(WeatherStation client) {
 4        System.out.printf("Average weather of this month:" +
 5                "\n Average Temperature %.2f celsius" +
 6                "\n Average Humidity %.2f" +
 7                "\n Average Pressure %.2f\n",
 8            client.getTemperature(),
 9            client.getHumidity(),
10            client.getPressure());
11    }
12}

客户端 #

 1public class WeatherStationClient {
 2    public static void main(String[] args) {
 3        WeatherStation client = new WeatherStation();
 4        client.setData(23.2f, 10.91f, 1.01f);
 5        NormalBoard normalBoard =  new NormalBoard();        
 6        client.registerBoard(normalBoard);
 7        // register a new listener
 8        StatisticsBoard statisticsBoard =  new StatisticsBoard();
 9        client.registerBoard(statisticsBoard);
10        
11        client.notifyBoard();   // 1st notify
12        client.unregisterBoard(normalBoard);
13        client.setStatus(true);
14        client.notifyBoard();   // 2nd notify
15    }
16}

上述示例代码的输出为:

 1Today's weather:
 2 Temperature 23.20 celsius
 3 Humidity 10.91
 4 Pressure 1.01
 5Average weather of this month:
 6 Average Temperature 23.20 celsius
 7 Average Humidity 10.91
 8 Average Pressure 1.01
 9Average weather of this month:
10 Average Temperature 23.20 celsius
11 Average Humidity 10.91
12 Average Pressure 1.01

可以看到,第一次notify时,2个订阅者都更新了信息。

当移除一个订阅者后,再次notify,只有一个订阅者更新了信息。

另一个订阅者NormalBoard的代码并没给出,很简单,就省略了。