《Head First 设计模式》:观察者模式

176 阅读4分钟

正文

一、定义

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

要点:

  • 观察者模式定义了对象之间一对多的关系。
  • 观察者模式让主题(可观察者)和观察者之间松耦合。
  • 主题对象管理某些数据,当主题内的数据改变时,会以某种形式通知观察者。
  • 观察者可以订阅(注册)主题,以便在主题数据改变时能收到更新。
  • 观察者如果不想收到主题的更新通知,可以随时取消订阅(注册)。

二、实现步骤

1、创建主题父类/接口

主题父类/接口主要提供了注册观察者、移除观察者、通知观察者三个方法。

/**
 * 主题
 */
public class Subject {
    
    /**
     * 观察者列表
     */
    private ArrayList<Observer> observers;
    
    public Subject() {
        observers = new ArrayList<>();
    }

    /**
     * 注册观察者
     */
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    /**
     * 移除观察者
     */
    public void removeObserver(Observer o) {
        observers.remove(o);        
    }

    /**
     * 通知所有观察者,并推送数据(也可以不推送数据,而是由观察者过来拉取数据)
     */
    public void notifyObservers(Object data) {
        for (Observer o : observers) {
            o.update(data);
        }
    }
}

2、创建观察者接口

观察者接口主要提供了更新方法,以供主题通知观察者时调用。

/**
 * 观察者接口
 */
public interface Observer {

    /**
     * 根据主题推送的数据进行更新操作
     */
    public void update(Object data);
}

3、创建具体的主题,并继承主题父类/实现主题接口

/**
 * 主题A
 */
public class SubjectA extends Subject {
    
    /**
     * 主题数据
     */
    private String data;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
        // 数据发生变化时,通知观察者
        notifyObservers(data);
    }
}

4、创建具体的观察者,并实现观察者接口

通过观察者类的构造函数,注册成为主题的观察者。

(1)观察者 A

/**
 * 观察者A
 */
public class ObserverImplA implements Observer {

    private Subject subject;
    
    public ObserverImplA(Subject subject) {
        // 保存主题引用,以便后续取消注册
        this.subject = subject;
        // 注册观察者
        subject.registerObserver(this);
    }
    
    @Override
    public void update(Object data) {
        System.out.println("Observer A:" + data.toString());
    }
}

(2)观察者 B

/**
 * 观察者B
 */
public class ObserverImplB implements Observer {

    private Subject subject;
    
    public ObserverImplB(Subject subject) {
        // 保存主题引用,以便后续取消注册
        this.subject = subject;
        // 注册观察者
        subject.registerObserver(this);
    }
    
    @Override
    public void update(Object data) {
        System.out.println("Observer B:" + data.toString());
    }
}

5、使用主题和观察者对象

public class Test {
    
    public static void main(String[] args) {
        // 主题
        SubjectA subject = new SubjectA();
        // 观察者A
        ObserverImplA observerA = new ObserverImplA(subject);
        // 观察者B
        ObserverImplB observerB = new ObserverImplB(subject);
        // 模拟主题数据变化
        subject.setData("I'm Batman!!!");
        subject.setData("Why so serious...");
    }
}

三、举个栗子

1、背景

你的团队刚刚赢得一纸合约,负责建立 Weather-O-Rama 公司的下一代气象站——Internet 气象观测站。

该气象站建立在 WeatherData 对象上,由 WeatherData 对象负责追踪目前的天气状况(温度、湿度、气压)。并且具有三种布告板,分别显示目前的状况、气象统计以及简单的预报。当 WeatherData 对象获得最新的测量数据时,三种布告板必须实时更新。

并且,这是一个可扩展的气象站,Weather-O-Rama 气象站希望公布一组 API,好让其他开发人员可以写出自己的气象布告板,并插入此应用中。

2、实现

(1)创建主题父类

/**
 * 主题
 */
public class Subject {

    /**
     * 观察者列表
     */
    private ArrayList<Observer> observers;
    
    public Subject() {
        observers = new ArrayList<>();
    }
    
    /**
     * 注册观察者
     */
    public void registerObserver(Observer o) {
        observers.add(o);        
    }

    /**
     * 移除观察者
     */
    public void removeObserver(Observer o) {
        observers.remove(o);        
    }

    /**
     * 通知所有观察者,并推送数据
     */
    public void notifyObservers(float temperature, float humidity, float pressure) {
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }
}

(2)创建观察者接口

/**
 * 观察者接口
 */
public interface Observer {

    /**
     * 更新观测值
     */
    public void update(float temperature, float humidity, float pressure);
}

(3)创建气象数据类,并继承主题父类

/**
 * 气象数据
 */
public class WeatherData extends Subject {
    
    /**
     * 温度
     */
    private float temperature;
    /**
     * 湿度
     */
    private float humidity;
    /**
     * 气压
     */
    private float pressure;
    
    public void measurementsChanged() {
        // 观测值变化时,通知所有观察者
        notifyObservers(temperature, humidity, pressure);
    }
    
    /**
     * 设置观测值(模拟观测值变化)
     */
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

(4)创建布告板,并实现观察者接口

/**
 * 目前状态布告板
 */
public class CurrentConditionsDisplay implements Observer {
    
    private Subject weatherData;
    private float temperature;
    private float humidity;
    
    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        // 注册观察者
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    public void display() {
        System.out.println("Current conditions:" + temperature + "F degress and " + humidity + "% humidity");
    }
}
/**
 * 统计布告板
 */
public class StatisticsDisplay implements Observer {

    private Subject weatherData;
    private ArrayList<Float> historyTemperatures;
    
    public StatisticsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        // 注册观察者
        weatherData.registerObserver(this);
        historyTemperatures = new ArrayList<>();
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.historyTemperatures.add(temperature);
        display();
    }
    
    public void display() {
        if (historyTemperatures.isEmpty()) {
            return;
        }
        Collections.sort(historyTemperatures);
        float avgTemperature = 0;
        float maxTemperature = historyTemperatures.get(historyTemperatures.size() - 1);
        float minTemperature = historyTemperatures.get(0);
        float totalTemperature = 0;
        for (Float temperature : historyTemperatures) {
            totalTemperature += temperature;
        }
        avgTemperature = totalTemperature / historyTemperatures.size();
        System.out.println("Avg/Max/Min temperature:" + avgTemperature + "/" + maxTemperature + "/" + minTemperature);
    }
}
/**
 * 预测布告板
 */
public class ForecastDisplay implements Observer {

    private Subject weatherData;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public ForecastDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        // 注册观察者
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }
    
    public void display() {
        System.out.println("Forecast:waiting for implementation...");
    }
}

(5)测试

public class Test {
    
    public static void main(String[] args) {
        // 气象数据
        WeatherData weatherData = new WeatherData();
        // 目前状态布告板
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        // 统计布告板
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
        // 预测布告板
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
        
        // 模拟气象观测值变化
        weatherData.setMeasurements(80, 65, 30.4F);
        weatherData.setMeasurements(82, 70, 29.2F);
        weatherData.setMeasurements(78, 90, 29.2F);
    }
}