设计之禅——观察者模式

引言

观察者模式也是非常好理解的模式之一,因为在生活中很容易找到类比,比如报纸、书刊订阅,手机app消息通知等等,所以仅通过名字大致也就能明白这个模式的作用。不过,从代码的层次来讲却有非常多的细节需要注意。

定义

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

通过以上定义我们不难发现观察者模式一定存在两个角色,一个是被观察的对象,我们称之为主题,也就是上述所说的“一”;而另一个则是观察者对象,也就是上述所说的“多”。
当主题状态改变时,所有的观察者都会收到通知,要实现这点应该如何做呢?继承OR组合?如果你看过我之前的文章,应该还会记得多用组合,少用继承这一基本设计原则,使用组合能够大大减少两个对象之间的耦合度,我们应该为减少对象之间的耦合度而努力。那么此处只需要让主题持有观察者的引用,当状态改变时,由主题调用观察者的更新方法,达到通知的目的。
主题需要持有观察者的引用,但是观察者的数量、类型并不确定啊,难道每增加一个观察者我们就需要更改主题的代码并将其组合进去吗?但唯一可以确定的是他们都会有一个共同的更新数据的方法供主题调用,所以我们只需要让观察者们都实现一个接口,主题持有该接口的引用即可,也就是面向对象设计的另一个原则针对接口编程,而不是针对实现编程。同时,主题也应该会有多个不是吗?所以我们得让主题也都实现自同一的接口,方便以后扩展。因此,也就有了下面这张类图:观察者模式类图
理论讲完,接下来我们编程实现一下吧。

Coding

《Head First设计模式》中对于观察者模式的讲解实例“天气系统”非常好,我这里也就直接采用了。首先需要一个主题接口:

public interface Subject {
	// 使用List来保存观察者
    ArrayList<Observer> observers = new ArrayList<>();

    default void register(Observer observer) {
        observers.add(observer);
    }

    default void remove(Observer observer) {
        observers.remove(observer);
    }

    /**
     * 主题将所有数据推送给观察者
     *
     *@param object 需要更新的数据封装的对象
     *@date 2018-11-17
     *
     */
    default void notifyAllObservers(Object object) {
        for (Observer observer : observers) {
        	// 通知观察者
            observer.update(this, object);
        }
    }

}

对于主题族,它们一定都需要一个保存对观察的引用,而且是多个,所以这里使用ArrayList来存储,同时,主 题也都会有注册观察者、移除观察者以及通知观察者这个三个同样的方法,我给它们添加了默认实现,就不用每个子类再单独的实现,做重复的工作了。(注意:notifyAllObservers这个方法在《HeadFirst设计模式》中是将参数一个个传入的,那样确实便于理解,但也就仅限于教学使用,我这里就不再重复了,直接实现了一个通用的接口,可以在任何场景直接使用,若难以理解请查阅原书。)

public interface Observer {

    void update(Subject subject, Object arg);

}

观察者接口就比较简单了,就只有一个方法update,同时需要传入两个参数,一个是主题本身的对象,另一个是需要更新的数据对象,这里先忽略前面的参数,稍后我会再着重讲这点。

public class WeateherData implements Subject {

    private double temperature;
    private double pressure;
    private double humidity;

    public double getTemperature() {
        return this.temperature;
    }
    public double getPressure() {
        return this.pressure;
    }
    public double getHumidity() {
        return this.humidity;
    }

	// 改变天气系统的数据并通知观察者
    public void changeData(double temperature, double pressure, double humidity) {
        this.temperature = temperature;
        this.pressure = pressure;
        this.humidity = humidity;

        // 主题决定需要推送哪些数据
        notifyAllObservers(this);
    }
}

具体的主题类,这里就是天气系统类,包含了温度、湿度以及气压三个数据,每当调用changeData方法改变数据时都会通知观察者,因为这里是由主题决定要推送哪些数据过去(为了方便,这里直接将本身对象传入过去),而观察者是被动接收,所以称这种方式为“推”(稍后会看到观察者如何“拉”数据)。

public class CurrentConditionDisplay implements Observer {
    private double temperature;
    private double pressure;
    private double humidity;

    private Subject subject;

    public CurrentConditionDisplay(Subject subject) {
    	// 这里保存对主题的引用,是为了方便将来需要注销观察者身份等操作
        this.subject = subject;
        subject.register(this);
    }

    @Override
    public void update(Subject subject, Object arg) {
        // 观察者被动接收主题传送的数据,因为将来会有很多主题,所以这里需要检查类型
        if (arg instanceof WeateherData) {
            WeateherData weateherData = (WeateherData) arg;
            this.temperature = weateherData.getTemperature();
            this.pressure = weateherData.getPressure();
            this.humidity = weateherData.getHumidity();

            display();
        }

    private void display() {

        System.out.println("temperature: " + temperature + "pressure: " + pressure + "humidity: " + humidity);

    }

}

这里实现了一个“当前天气情况布告板”,当新建一个布告板时,我们传入需要关注的主题,调用它的注册方法,那么它也就可以接收到主题的推送了。我们可以看到它也需要展示空气、湿度、气压三个数据,所以,对于主题将所有参数传递过来没有什么问题,但若是它只需要其中一个或两个数据,那另外一个传递的数据不也就多余了么?换到现实生活中来讲,也就是你会不停的接收到你不需要的信息,不胜其扰,所以相对于主题主动推送数据,观察者也可以自己决定从主题那获取需要的数据,那要怎么做呢?嘿 ,别忘了刚刚我们看到update除了传递参数对象过来,还可以将主题本身传递过来啊,这样观察者需要什么自己拉取就行了,对此我们需要对主题的notifyAllObservers和观察者的update方法做点小小的改动。

default void notifyAllObservers() {
	for (Observer observer : observers) {
        observer.update(this, null);
    }
}

public void update(Subject subject, Object arg) {
	 // 观察者自己拉取所需数据
    if (subject instanceof WeateherData) {
        WeateherData weateherData = (WeateherData) subject;
        this.temperature = weateherData.getTemperature();
        this.pressure = weateherData.getPressure();
        this.humidity = weateherData.getHumidity();

        display();
    }
}

至此,观察者模式就通过代码实现好了,那么Java中哪里有用到呢?

Java中的观察者模式

在JavaAPI中本身也提供了对观察者模式的支持,即java.util包中的Observable和Observer,Observable就是主题,Observer就是观察者接口,不过需要注意的是,Observable是一个类而非接口,这就具有一定的局限性了,如果我们的主题类需要扩展其他类的功能就没法实现了;同时在这个类里面还有一个changed属性和setChanged方法,那这两个是做什么用的呢?

private boolean changed = false;
protected synchronized void setChanged() {
	changed = true;
}

用法很简单,想象一下,我们推送或拉取数据很多时候是不需要时时进行的,比如,温度改变了0.00000001度立马就推送过去,这样观察者接收到的通知消息就太多了,那有了这个属性,我们就可以在达到一定条件时再通知观察者,也就是当changed=true时再调用update方法。不过还没完,我想你也注意到了Java原生的API将这个方法设置为了protected,也就是说只有其子类才能使用,想在外部使用就没法了,不过,我们已经掌握了观察者模式的实现,如果需要我们可以自己实现一套而不必非得使用Java原生的API。

总结

观察者模式也是非常的简单,当多个对象依赖于一个对象的状态改变时我们就可以使用该模式。
对于是使用“推”还是“拉”的方式,完全取决于实际的业务场景,当主题需要保护自身数据的安全时就可以采取“推”的方式,反之且当观察者不想接收过多无用的数据时就可以采用“拉”的方式。
在JavaAPI中提供了对观察者模式的支持,但需要注意的是主题不再是一个接口,而是一个类,也就有点违背了“针对接口编程,而非针对实现编程”原则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值