观察者模式 Observe Pattern

一、模式介绍

1.1、定义

观察者(Observe)模式定义了对象之间的一对多的依赖,这样一来,当一个对象改变状态是,它的所有依赖者都会收到通知并自动更新。观察者模式提供一个对象设计,让主题和观察者之间松耦合。

这种模式有时又称作发布-订阅模式,出版者(主题)+ 订阅者(观察者)= 观察者模式。实现观察者模式时,要主要具体目标对象和具体观察者对象之间不能直接调用,否则会使两者之间紧密耦合起来,这违反了面向对象的设计原则。

1.2、优点

  1. 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
  2. 目标与观察者之间建立了一套触发机制

1.3、缺点

  1. 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用
  2. 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率

二、结构与实现

2.1、结构

  1. 抽象主题(Subject)角色:也叫抽象目标类,对象使用此接口注册为观察者,或把自己从观察者中删除,以及通知所有观察者的抽象方法。每个主题可以有许多观察者
  2. 具体主题(ConcreteSubject)角色:也叫具体目标类,一个具体主题总是实现主题接口,除了注册和撤销方法之外,还实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。具体的主题也可能有设置和获取状态的方法
  3. 抽象观察者(Observer)角色:它是一个抽象类或接口,所有潜在的观察者必须实现这个接口;它包含了一个更新自己的抽象方法,当主题状态改变时,被调用
  4. 具体观察者(ConcreteObserver)角色:具体的观察者实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。观察者必须注册具体主题,以便接收更新
    在这里插入图片描述

2.2、实现一:自己实现 Observer

设计一个气象站应用,有三种布告分别显示目前的状况、气象统计及简单的预报,三种必须实时更新,而且这是一个可以扩展的气象站,可以根据气象站的API,写出自己的气象公告板。

2.2.1、类图

在这里插入图片描述

2.2.2、Subject
package com.erlang.subject.my;

import java.util.ArrayList;
import java.util.List;

/**
 * @description: 抽象主体
 * @author: erlang
 * @since: 2022-02-18 08:09
 */
public abstract class Subject {
    /**
     * 记录观察者,此List 是在构造器中建立
     */
    protected List<Observer> observers;

    public Subject() {
        observers = new ArrayList<>();
    }


    /**
     * 当注册观察者时,我们只要把它放在List 集合中即可
     *
     * @param observer 观察者
     */
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    /**
     * 管观察者取消订阅,我们把它从List 中删除即可
     *
     * @param observer 观察者
     */
    public void removeObserver(Observer observer) {
        int i = observers.indexOf(observer);
        if (i > 0) {
            observers.remove(observer);
        }
    }

    /**
     * 当主题状态改变时,这个方法会被调用,已通知所有的观察者
     */
    public abstract void notifyObservers();
}
2.2.3、Observer
package com.erlang.subject.my;

/**
 * @description: 自己实现的观察者接口
 * 所有观察者都必须实现此方法,以实现观察者接口
 * @author: erlang
 * @since: 2022-02-18 08:10
 */
public interface Observer {
    /**
     * 当气象观测值改变时,主题会把这些状态值当作方法的餐朱,传给观察者
     *
     * @param temperature 温度
     * @param humidity 湿度
     * @param pressure 气压
     */
    void update(float temperature, float humidity, float pressure);
}
2.2.4、DisplayElement
package com.erlang.subject.my;

/**
 * @description: 当布告板需要显示时,需要实现此接口
 * @author: erlang
 * @since: 2022-02-18 08:11
 */
public interface DisplayElement {
    /**
     * 当布告板需要显示时,调用此方法
     */
    void display();
}
2.2.5、CurrentConditionsDisplay
package com.erlang.subject.my;

/**
 * @description: 此布告实现了IObserver 接口,可以从 WeatherData 对象中获得改变
 * 也实现了 DisplayElement 接口,因为我们的 API 规定所有的布告都必须实现此接口
 * @author: erlang
 * @since: 2022-02-18 08:12
 */
public class CurrentConditionsDisplay implements DisplayElement, Observer {

    /**
     * 温度
     */
    private float temperature;

    /**
     * 湿度
     */
    private float humidity;

    /**
     * 气压
     */
    private float pressure;

    /**
     * 主体
     */
    private Subject subject;

    /**
     * 可以在创建该对象时,就注册观察者
     *
     * @param subject
     */
    public CurrentConditionsDisplay(Subject subject) {
        this.subject = subject;
    }

    /**
     * 注册观察者
     */
    public void registerObserver() {
        subject.registerObserver(this);
    }

    /**
     * 当此方法被调用时,我们把温度和湿度保存起来,然后调用display()
     *
     * @param temperature 温度
     * @param humidity    湿度
     * @param pressure    气压
     */
    @Override
    public void update(float temperature, float humidity, float pressure) {
        System.out.println();
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    /**
     * 展示最近的温度和湿度
     */
    @Override
    public void display() {
        System.out.println("当前天气情况:气温 " + temperature + " 度,湿度:" + humidity + "%,气压:" + pressure);
        System.out.println("--------------------------------------------------------------------------------------");

    }
}
2.2.6、WeatherData
package com.erlang.subject.my;

/**
 * @description: 气象台
 * @author: erlang
 * @since: 2022-02-18 08:14
 */
public class WeatherData extends Subject {

    /**
     * 温度
     */
    private float temperature;

    /**
     * 湿度
     */
    private float humidity;

    /**
     * 气压
     */
    private float pressure;

    /**
     * 把状态告诉每一个观察者,因为所有观察者都是实现了update() 方法,所以知道如何通知他们
     */
    @Override
    public void notifyObservers() {
        if (observers.size() > 0) {
            for (Observer observer : observers) {
                observer.update(temperature, humidity, pressure);
            }
        }
    }

    /**
     * 当气象站得到更新观测值时,我们通知观察者
     */
    public void measurementsChanged() {
        notifyObservers();
    }

    /**
     * 设置温度
     *
     * @param temperature 温度
     * @param humidity    湿度
     * @param pressure    气压
     */
    public void setMeasurements(float temperature, float humidity, float pressure) {
        System.out.println("#### WeatherData.setMeasurements:天气变化通知...");
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}
2.2.7、启动气象站
package com.erlang.subject.my;

/**
 * @description: 气象站
 * @author: erlang
 * @since: 2022-02-18 08:18
 */
public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay display = new CurrentConditionsDisplay(weatherData);
        // 注册观察者
        display.registerObserver();
        // 模拟新的气象测量
        weatherData.setMeasurements(30, 65, 30.1f);
        weatherData.setMeasurements(29, 67, 32.1f);
        weatherData.setMeasurements(27, 63, 31.1f);
    }
}
2.2.8、运行结果
#### WeatherData.setMeasurements:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 30.0 度,湿度:65.0%,气压:30.1
#### WeatherData.setMeasurements:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 29.0 度,湿度:67.0%,气压:32.1
#### WeatherData.setMeasurements:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 27.0 度,湿度:63.0%,气压:31.1

2.3、实现二:Java 内置的 Observer

2.3.1、Observer

对象实现该接口称为观察者。观察者接收数据的动作子类对 update 的实现,其中第一个参数是主题本身,是让观察者知道是哪个主题通知它;第二个参数是 Observable::notifyObservers 方法的入参,如果没有说明则为空。

// java.util.Observer
public interface Observer {
	void update(Observable o, Object arg);
}
2.3.2、Observable 部分代码

观察者注册见 addObserver 方法;删除见 Observable::deleteObserver 方法。可观察者发出通知的动作见 notifyObservers 方法,在调用该方法前需要先调用 setChanged,标记可观察者的状态为允许发送状态。

public class Observable {
	// ....
	// 注册观察者
	public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }
    // 删除观察者
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }
	// 发送给所有观察者
	public void notifyObservers() {
	 	notifyObservers(null);
	}
	// 发送指定数据给所有的观察者
	public void notifyObservers(Object arg) {
	    /*
	     * 缓存所有观察者
	     */
	    Object[] arrLocal;
	
	    synchronized (this) {
	        /* 当状态改变时,就下面的运行
	         */
	        if (!changed)
	            return;
	        arrLocal = obs.toArray();
	        clearChanged();
	    }
		/* 
		* 通知每一个观察者
		*/
	    for (int i = arrLocal.length-1; i>=0; i--)
	        ((Observer)arrLocal[i]).update(this, arg);
	}
	// 标记状态已改变,允许发送通知给观察者
	protected synchronized void setChanged() {
		changed = true;
	}
	// ....
}
2.3.3、类图

在这里插入图片描述

2.3.4、WeatherData
package com.erlang.subject.sys;

import java.util.Observable;

/**
 * @description: 继承 Observable 类
 * 这里不再需要追踪观察者了,也不需要管理观察者的注册与删除(父类中已经实现了这些功能)
 * @author: erlang
 * @since: 2022-02-18 08:26
 */
public class WeatherData extends Observable {
    /**
     * 温度
     */
    private float temperature;

    /**
     * 湿度
     */
    private float humidity;

    /**
     * 气压
     */
    private float pressure;


    public WeatherData() {

    }

    /**
     * 当气象站得到更新观测值时,我们通知观察者
     */
    public void measurementsChanged() {
        System.out.println("#### WeatherData.measurementsChanged:天气变化通知...");
        // 调用 notifyObservers() 方法之前需要先调用setChanged() 方法,来指示状态已经改变
        setChanged();
        notifyObservers();
    }


    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }

}
2.3.5、CurrentConditionsDisplay
package com.erlang.subject.sys;

import com.erlang.subject.my.DisplayElement;

import java.util.Observable;
import java.util.Observer;

/**
 * @description: 此布告实现了java.util.Observer 接口
 * 也实现了 DisplayElement 接口,因为我们的 API 规定所有的布告都必须实现此接口
 * @author: erlang
 * @since: 2022-02-18 08:27
 */
public class CurrentConditionsDisplay implements Observer, DisplayElement {
    /**
     * 温度
     */
    private float temperature;

    /**
     * 湿度
     */
    private float humidity;

    /**
     * 气压
     */
    private float pressure;

    /**
     * 观察者中心
     */
    private Observable observable;

    /**
     * 可以在创建该对象时,就注册观察者
     *
     * @param observable
     */
    public CurrentConditionsDisplay(Observable observable) {
        this.observable = observable;
    }

    /**
     * 注册观察者
     */
    public void registerObserver() {
        observable.addObserver(this);
    }


    /**
     * 当此方法被调用时,我们把温度和湿度保存起来,然后调用display()
     *
     * @param o
     * @param arg
     */
    @Override
    public void update(Observable o, Object arg) {
        System.out.println("#### CurrentConditionsDisplay.update:接收到天气变化通知...");
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }

    /**
     * 展示最近的温度和湿度
     */
    @Override
    public void display() {
        System.out.println("#### CurrentConditionsDisplay.display:当前天气情况:气温 " + temperature + " 度,湿度:" + humidity + "%,气压:" + pressure);
        System.out.println("--------------------------------------------------------------------------------------");
    }
}
2.3.6、WeatherStation
package com.erlang.subject.sys;

/**
 * @description: 气象站
 * @author: erlang
 * @since: 2022-02-18 08:30
 */
public class WeatherStation {
    public static void main(String[] args) {

        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        // 注册观察者
        currentDisplay.registerObserver();

        /**
         * 模拟新的气象测量
         */
        weatherData.setMeasurements(30, 65, 30.1f);
        weatherData.setMeasurements(29, 67, 32.1f);
        weatherData.setMeasurements(27, 63, 31.1f);
    }
}
2.3.7、执行结果
#### WeatherData.measurementsChanged:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 30.0 度,湿度:65.0%,气压:30.1
--------------------------------------------------------------------------------------
#### WeatherData.measurementsChanged:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 29.0 度,湿度:67.0%,气压:32.1
--------------------------------------------------------------------------------------
#### WeatherData.measurementsChanged:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 27.0 度,湿度:63.0%,气压:31.1
--------------------------------------------------------------------------------------
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值