【一起学系列】之观察者模式:我没有在监控你啊

2,215 阅读5分钟

意图

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

别名:发布-订阅模式

观察者模式的诞生

将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的致性,我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。

说人话就是:

【产品】:开发小哥,我需要你设计一个天气预报显示大屏,气象站会给你发送数据,你需要把它展示到大屏里,OK吗?

【开发】:OJBK!秒秒钟搞定一切!代码立马出来!

void getTemperature () {
    // 从气象站获取发送过来的温度数据
    // getData();
    
    
    // ................................
    // 显示到大屏里面去
    // showDataToScreen();
    // ................................
}

void getMisture () {
    // 从气象站获取发送过来的湿度数据
    // getData();
    
    
    // ................................
    // 显示到大屏里面去
    // showDataToScreen();
    // ................................
}

void getAirindex () {
    // 从气象站获取发送过来的空气指数数据
    // getData();
    
    
    // ................................
    // 显示到大屏里面去
    // showDataToScreen();
    // ................................
}

【BOSS】:磕大头!宁是准备每获取一次数据就把代码CV一遍吗?你不累吗?

【开发】:老大,我一点都不累!就是复制粘贴一下呀!

【BOSS】:如果我现在不需要同步更新天气指数呢?删代码吗?

【开发】:对啊!一秒钟就能删掉!( •̀ ω •́ )✧

【BOSS】:重写😃

HeadFirst 核心代码

于是乎,我们开启了关于设计模式的经典书籍阅读之旅

/**
 * 观察主题接口
 */
public interface Observable{
    public void addObserver(Observer observer);     // 添加观察者
    public void removeObserver(Observer observer);  // 移除观察者
    public void notifyObservers(WeatherData data);  // 通知所有观察者
}


/**
 * 观察者
 */
public interface Observer {
    public abstract void update(WeatherData data);
}


/**
 * 天气主题
 *
 */
public class Weather implements Observable {
    private List<Observer> observers = new ArrayList<>();
    
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    public void notifyObservers(WeatherData data) {
        for (Observer observer : observers)
            observer.update(data);
    }
}

观察者模式的设计思路:

  • Subject 目标(容器)提供注册和删除观察者的接口以及更新接口
  • Observer(观察者)为那些在目标发生改变时需获得通知的对象定义一个更新接口
  • ConcreteSubject(具体目标)状态发生改变时,向各个观察者发出通知
  • ConcreteObserver(具体观察者)实现Observer的更新接口

简单来说,

  1. 我们需要一个接口来定义注册,删除和更新接口
  2. 然后由具体的目标(类)实现该接口,并且在类中创建一个容器,存储需要被通知的对象
  3. 需要被通知的对象,需要实现Observer接口中的update更新方法
  4. 将观察者对象注册进容器中,当具体目标更新时,调用所有容器类对象的update方法

如果看着有点模棱两可,就看完本文后,访问专题设计模式开源项目,里面有具体的代码示例,链接在最下面

JDK中的观察者模式

JDK中已经对观察者模式有具体的实现,代码非常简单,如下所示:

具体目标:

public class ObservableApp extends Observable {

    private long curr;
    
    public ObservableApp(long curr) {
        this.curr = curr;
    }
    
    public void change(long newStr) {
        this.curr = newStr;
        
        // 更改状态,发送通知
        setChanged();
        notifyObservers(newStr);
    }

    @Override
    protected synchronized void setChanged() {
        super.setChanged();
    }
}

具体观察者:

public class ObserverA implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        System.out.println(MessageFormat.format("ObserverA -> {0} changed, Begin to Work. agr is:{1}", o.getClass().getSimpleName(), arg));
    }
}

Main方法:

public class App {
    
    public static void main(String[] args) throws InterruptedException {
        ObservableApp app = new ObservableApp(System.currentTimeMillis());
        System.out.println(app.getCurr());
        app.addObserver(new ObserverA());
        app.addObserver(new ObserverB());

        Thread.sleep(1000);
        
        long curr = System.currentTimeMillis();
        app.change(curr);
        System.out.println(app.getCurr());
    }
}

// 输出如下:
// 1589688733464
// ObserverB -> ObservableApp changed, Begin to Work. agr is:1,589,688,734,469
// ObserverA -> ObservableApp changed, Begin to Work. agr is:1,589,688,734,469
// 1589688734469

推模式

通知发给观察者,通知携带参数,这就是推,对应JDK方法中的:notifyObservers(Object arg)

拉模式

通知发给观察者,通知不携带参数,需要观察者自己去主动调用get方法获取数据,这就是拉

对应JDK方法中的:notifyObservers(),仅告知观察者数据发生了变化,至于数据的详情需要观察者主动到主题中pull数据

拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。另一方面。拉模型可能效率较差,因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。

遵循的设计原则

  1. 封装变化
    • 在观察者模式中会经常改变的是主题的状态,以及观察者的数目和类型
    • 我们可以改变依赖于主题状态的对象,但是不必改变主题本身,这便是提前规划
  2. 针对接口编程
    • 主题和观察者都使用了接口
    • 观察者利用主题的接口向主题注册
    • 主题利用观察者接口通知观察者,可以使两者之间正常交互,同时又具有松耦合的特性
  3. 多使用组合
    • 观察者模式利用组合将许多观察者组合进主题中
    • 它们之间的关系并不是通过继承得到,而是在运行时动态改变

什么场景适合使用

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern),比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式

Code/生活中的实际应用

  • 比如微信公众号中的订阅关注,订阅后,公众号发布文章会实时分发给各个账号
  • 又如,我们使用Keep跑步时,如果你跑的足够激情,它会提示你,恭喜你,你已经打破了五公里的最好记录!这样的语音提醒一定是触发式,而不是实时去检测吧?(实时检测没有意义,浪费性能)这里就可以利用观察者模式进行设计和解耦

最后

附上GOF一书中对于观察者模式的UML图:

观察者模式UML图
观察者模式UML图

相关代码链接

GitHub地址

  • 兼顾了《HeadFirst》以及《GOF》两本经典数据中的案例
  • 提供了友好的阅读指导