观察者模式

观察者模式

气象站项目

好消息,你们公司和气象站签订了一笔500W的项目,老板说只要顺利搞定了这个项目,奖励项目组成员到三亚晒太阳(6月的天…)。
需求很简单,气象站会搜集湿度、温度、气压等气象数据,然后放到一个叫WeatherData对象中,现在需要我们来监控WeatherData对象,当里面的气象数据发生变更后,要将变更显示到布告板上,而布告板目前需要提供的有"目前状态"布告板,"气象统计"布告板,"天气预报"布告板;同时系统要提供布告板的可扩展性,即可以非常容易的添加新的布告板,或删除已有的布告板。

先看看WeatherData类长什么样子,这是由气象站提供的。

/**
 * 天气数据
 *  由气象站提供的类
 */
public class WeatherData {

    private float temperature;  //温度
    private float humidity;     //湿度
    private float pressure;     //气压


    /**
     * 当新的气象数据准备妥当时,会调用这个方法
     * 此时就需要我们来更新布告板信息。
     */
    public void measurementsChanged(){
        //这儿就是我们需要去实现的代码。
    }


    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
    }

}

气象数据发生变更后,会调用measurementsChanged()这个方法(我们不用管是怎么调用的),我们需要实现这个方法,来通知布告板更新。

需求清楚后,我们就开始来设计吧。既然是measurementsChanged()方法要变更布告板信息,那我们在里面直接调用布告板的相关方法不就行了吗。

V1版本

基于这样的思路,我们来实现V1版本。布告板有多种类型,通过OO思想,我们非常容易可以抽象出来一个布告板的抽象出来。

/**
 * 布告板抽象
 */
public interface DisplayV1 {

    /**
     * 更新布告板
     * @param data 气象数据
     */
    void update(WeatherDTOV1 data);
}

数据对象WeatherDTOV1

/**
 * 气象数据封装
 */
public class WeatherDTOV1 {
    private float temperature;  //温度
    private float humidity;     //湿度
    private float pressure;     //气压

    public WeatherDTOV1(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("WeatherDTOV1{");
        sb.append("temperature=").append(temperature);
        sb.append(", humidity=").append(humidity);
        sb.append(", pressure=").append(pressure);
        sb.append('}');
        return sb.toString();
    }
}

实现需求需要的三个布告板

/**
 * 目前状况布告板
 */
public class CurrentConditionsDisplayV1 implements DisplayV1 {
    @Override
    public void update(WeatherDTOV1 data) {
        System.out.println("目标状况布告板更新,新数据:"+data.toString());
    }
}
/**
 * 气象统计布告板
 */
public class StatisticsDisplayV1 implements DisplayV1 {
    @Override
    public void update(WeatherDTOV1 data) {
        System.out.println("气象统计布告板更新,新数据:"+data.toString());
    }
}

/**
 * 天气预报布告板
 */
public class ForecastDisplayV1 implements DisplayV1 {
    @Override
    public void update(WeatherDTOV1 data) {
        System.out.println("天气预报布告板更新,新数据:"+data.toString());
    }
}

最后,我们在WeatherDataV1的measurementsChanged()方法中,依次调用布告板更新信息。

/**
 * 当新的气象数据准备妥当时,会调用这个方法
 * 此时就需要我们来更新布告板信息。
 */
public void measurementsChanged(){
    //这儿就是我们需要去实现的代码。
    WeatherDTOV1 weatherDTOV1 =
            new WeatherDTOV1(getTemperature(), getHumidity(), getPressure());

    CurrentConditionsDisplayV1 currentConditionsDisplay = new CurrentConditionsDisplayV1();
    currentConditionsDisplay.update(weatherDTOV1);

    StatisticsDisplayV1 statisticsDisplay = new StatisticsDisplayV1();
    statisticsDisplay.update(weatherDTOV1);

    ForecastDisplayV1 forecastDisplay = new ForecastDisplayV1();
    forecastDisplay.update(weatherDTOV1);
}

实现了我们的需求,可是没福利。

在这里插入图片描述

分析V1版本设计会存在什么问题?

  1. WeatherDataV1和具体布告板代码强耦合在一起,我们应该针对接口编程,而不是针对实现。
  2. 如果要新增或者移除布告板,那么就要修改WeatherDataV1的代码,不符合开闭原则。

这个场景其实是一个典型使用观察者模式的一个场景。
我们举个报纸的订阅例子,来理解下什么是观察者模式(也叫发布订阅模式)。

彭城晚报是一家报社出版的报纸,老周已经订阅了彭城晚报,那么每当有新的晚报出版后,都会送到老周手上。
当老周不再看晚报后,他可以取消订阅,这样报社就不会再给他送报纸了。只要这家报社还在,就一直会有人不断的订阅或者取消订阅报纸。

该场景对应到观察者模式中,报社称之为"主题",老周就是"观察者",观察者(老周)可以订阅(注册)到主题上,这样当主题状态发生变更时(出版新报纸),就会通知到已经订阅的观察者(老周)。观察者(老周)也可以取消订阅。

这样我们就可以抽象出主题对象,里面会有注册、取消注册观察者的方法,以及通知观察者的方法;而观察者有更新自己状态方法。

V2版本

好了,介绍完观察者是什么之后,用观察者来实现我们的天气项目。
观察者抽象(V1版本一致)

/**
 * 布告板抽象(就是观察者抽象,有一个更新自己状态的方法)
 */
public interface DisplayV2 {

    /**
     * 更新布告板
     * @param data 气象数据
     */
    void update(WeatherDTOV1 data);
}

观察者的实现同V1版本一样,就不用重复贴代码了。

主题抽象,有注册观察者、移除观察者、通知观察者的方法。

/**
 * 主题抽象
 */
public interface Subject {

    /**
     * 注册观察者(布告板)
     * @param display
     */
    void registerDisplay(DisplayV2 display);

    /**
     * 移除观察者(布告板)
     * @param display
     */
    void removeDisplay(DisplayV2 display);

    /**
     * 通知观察者(布告板)
     */
    void notifyDisplay();
}

主题实现,让WeatherDataV2实现主题接口。

/**
 * 天气数据
 *  由气象站提供的类
 */
public class WeatherDataV2 implements Subject{

    private float temperature;  //温度
    private float humidity;     //湿度
    private float pressure;     //气压

    private List<DisplayV2> displays = new ArrayList<>(5); //布告板列表(观察者列表)


    /**
     * 当新的气象数据准备妥当时,会调用这个方法
     * 此时就需要我们来更新布告板信息。
     */
    public void measurementsChanged(){
        //这儿就是我们需要去实现的代码。
        notifyDisplay();
    }


    @Override
    public void registerDisplay(DisplayV2 display) {
        displays.add(display);
    }

    @Override
    public void removeDisplay(DisplayV2 display) {
        displays.remove(display);
    }

    @Override
    public void notifyDisplay() {
        WeatherDTOV2 weatherDTO =
                new WeatherDTOV2(getTemperature(), getHumidity(), getPressure());

        displays.forEach(display -> display.update(weatherDTO));
    }
    
    //getter setter 略
}

来吧,测试一波看看

/**
 * V2版测试
 */
public class WeatherMainV2 {
    public static void main(String[] args) {
        WeatherDataV2 weatherData = new WeatherDataV2();

        //注册观察者
        weatherData.registerDisplay(new CurrentConditionsDisplayV2());
        weatherData.registerDisplay(new ForecastDisplayV2());
        weatherData.registerDisplay(new StatisticsDisplayV2());

        //气象数据发生变更
        weatherData.setTemperature(34.5f);
        weatherData.setHumidity(125.7f);
        weatherData.setPressure(32.4f);

        //调用我们实现的方法。通知观察者
        weatherData.measurementsChanged();
    }
}

nice,非常完美。

V2版本的UML图
在这里插入图片描述

使用观察者模式能不能解决V1版本的问题呢,首先Subject 和 WeatherDataV2对象只依赖 观察者抽象(DisplayV2),不依赖具体的观察者实现。实际上这就是依赖倒置原则的体现。
其次,现在我们要新增观察者时,只需要新提供一个观察者的实现(扩展类),然后注册到WeatherDataV2中即可,同理移除观察者,只需要取消注册即可,符合开闭原则。

nice,三亚半月游,妥妥的。

对于气象站这个项目,我们没有测试移除观察者,你可以自行实现体验下看看(注意的移除实现可能存在问题哦,你可以自己发现并解决)。

定义

观察者模式的定义,观察者模式定义了对象之前的一对多的关系,这样,当一的对象状态发生变化时,多的对象会接受到通知并更新。

我们用代码实现下
抽象观察者

/**
 * 抽象观察者
 */
public interface Observer {
    /**
     * 更新方法
     * @param event 事件信息
     */
    void update(Event event);
}

它通常会依赖一个数据对象,而且我们也常常叫它事件对象。

/**
 * 事件(封装数据)
 */
public class Event {
}

具体观察者实现

/**
 * 具体观察者A
 */
public class ConcreteObserverA implements Observer{
    @Override
    public void update(Event event) {
        System.out.println("具体观察者A接收到事件,更新......");
    }
}
/**
 * 具体观察者B
 */
public class ConcreteObserverB implements Observer{
    @Override
    public void update(Event event) {
        System.out.println("具体观察者B接收到事件,更新......");
    }
}

抽象主题,依赖抽象观察者,提供注册、移除、通知接口。

/**
 * 抽象主题
 */
public interface Subject {

    /**
     * 注册观察者
     * @param observer
     */
    void registerObserver(Observer observer);

    /**
     * 移除观察者
     * @param observer
     */
    void removeObserver(Observer observer);

    /**
     * 通知观察者
     */
    void notifyObservers();
}

具体主题

/**
 * 具体主题
 */
public class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        Event event = new Event();
        observers.forEach(observer -> observer.update(event));
    }
}

测试一波

/**
 * 观察者测试
 */
public class ObserverMain {

    public static void main(String[] args) {
        Subject subject = new ConcreteSubject();
        subject.registerObserver(new ConcreteObserverA());
        subject.registerObserver(new ConcreteObserverB());

        subject.notifyObservers();
    }
}

观察者模式的UML图

在这里插入图片描述

JDK内置实现

因为观察者模式非常的常用,JDK已经内置了观察者模式。
其抽象主题是java.util.Observable,它是一个抽象类,实现了添加、删除、通知 观察者的实现。
抽象观察者是java.util.Observer,它是一个接口,定义了更新方法。
你可以改造我们的V2版本,使用JDK内置的观察者模式来实现。

但通常我们很少使用内置的观察者模式,这是因为它的主题对象是一个抽象类,要使用它的功能时,必须继承它,
因为java语法不支持多继承,当类已经继承其他超类时就无能为力了。另外Observable将一些关键的方法设置为protected,这意味着只能通过继承来复用,而不能通过组合Observable,来使用其中的一些关键方法,违反了多用组合,少用继承的原则。

扩展示例

为了加深理解,我们再来实现一个微信公众号的场景。(https://codepumpkin.com/observer-design-pattern/)
有一个叫"设计模式"的微信公众号,假如有张三,李四,王五三个同学关注,那么每当公众号推送新的文章时,都会及时通知这3位观察者。
某天,张三取消关注,那么之后推送的新文章,张三不会再收到。

同样的,定义抽象观察者(粉丝)

/**
 * 抽象观察者
 */
public interface Observer {
    /**
     * 更新方法
     * @param event 事件信息
     */
    void update(Event event);
}

依赖的事件对象

/**
 * 事件(封装数据)
 */
public class Event {

    private String officialAccountName;
    private String articleName;

    public Event(String officialAccountName, String articleName) {
        this.officialAccountName = officialAccountName;
        this.articleName = articleName;
    }
    //setter getter 忽略
}    

具体观察者实现

/**
 * 粉丝
 */
public class Follower implements Observer {

    private String name;

    public Follower(String name) {
        this.name = name;
    }

    @Override
    public void update(Event event) {
        System.out.println(getName() + ",接受到公众号"+ event.getOfficialAccountName() +"推送的新文章:" + event.getArticleName());
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

抽象主题

/**
 * 抽象主题
 */
public interface Subject {

    /**
     * 注册观察者
     * @param observer
     */
    void registerObserver(Observer observer);

    /**
     * 移除观察者
     * @param observer
     */
    void removeObserver(Observer observer);

    /**
     * 通知观察者
     */
    void notifyObservers();
}

具体主题(公众号)

/**
 * 公众号(具体主题)
 */
public class OfficialAccount implements Subject {
    private String name;
    private List<Observer> observers;
    private String lastArticle;


    public OfficialAccount(String name) {
        this.name = name;
        observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
        System.out.println(name + "公众号新增粉丝...");
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
        System.out.println(name + "公众号掉粉...");
    }

    @Override
    public void notifyObservers() {
        Event event = new Event(name, lastArticle);
        observers.forEach(observer -> observer.update(event));
    }

    public void pushArticle(String articleName){
        this.lastArticle = articleName;
        notifyObservers();
    }
}

测试一波

/**
 * 公众号测试
 */
public class OfficialAccountMain {

    public static void main(String[] args) {
        OfficialAccount account = new OfficialAccount("设计模式");
        Observer zhangsan = new Follower("张三");
        account.registerObserver(zhangsan);

        account.registerObserver(new Follower("李四"));
        account.registerObserver(new Follower("王五"));

        System.out.println("推送新文章。。。");
        account.pushArticle("观察者模式");

        account.removeObserver(zhangsan);
        System.out.println("推送新文章。。。");
        account.pushArticle("策略模式");
    }
}

测试结果:

设计模式公众号新增粉丝...
设计模式公众号新增粉丝...
设计模式公众号新增粉丝...
推送新文章。。。
张三,接受到公众号设计模式推送的新文章:观察者模式
李四,接受到公众号设计模式推送的新文章:观察者模式
王五,接受到公众号设计模式推送的新文章:观察者模式
设计模式公众号掉粉...
推送新文章。。。
李四,接受到公众号设计模式推送的新文章:策略模式
王五,接受到公众号设计模式推送的新文章:策略模式

源码

https://gitee.com/cq-laozhou/design-pattern

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值