2.观察者模式

1.问题分析

有趣的事情发生时,可千万别错过了!有一个模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事。对象甚至在运行时可决定是否要继续被通知。观察者模式是JDK中使用最多的模式之一,非常有用。我们也会-一并介绍一对多关系,以及松耦合(对,没错,我们说耦合)。有了观察者,你将会消息灵通。

1.1 问题需求

建立一个Internet气象观测站,该气象站必须建立在WeatherData对象上,由WeatherData对象负责追踪目前的天气状况(温度、湿度、气压)。建立一个应用,有三种布告板用于显示:

  1. 目前的状况
  2. 气象统计
  3. 简单的预报

当WeatherObject对象获得最新的测量数据时,三种布告板必须实时更新。

1.2 应用概况

此系统中的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(追踪来自气象站的数据、并更新布告板)和布告板(显示目前天气状况给用户看)

         WeatherData对象知道如何跟物理气象站联系,以取得更新的数据。WeatherData对象会随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。        

          如果我们选择接受这个项目,我们的工作就是建立一个应用,利用WeatherData对象取得数据,并更新三个布告板:目前状况、气象统计和天气预报。

1.3 WeatherData类的最初模板

 1.4 需求分析

 重点:一旦WeatherData有新的测量,这些布告必须马上更新。系统还需要支持可扩展。

1.5 错误示范

错误示范的实现有什么问题?

对于更新布告板的更新操作,全是 针对具体实现 编程,这会导致以后在增加或删除布告板时,必须修改程序

注:因为WeatherData已经类似底层的对象结构了,类似于ArrayList这种的,总不能说,以后更新的时候,还需要对这种底层类进行修改吧,以后对代码进行修改和维护,是针对执行代码中进行修改,如在main中修改即可,而无需修改这类底层类。 

每个布告板显示更新,看起来像是一个统一的接口,布告板的方法名称都是update(),参数都是温度、湿度和气压。

2.观察者模式

2.1 认识观察者模式

我们看看报纸和杂志的订阅是怎么回事:

  1. 报社的业务就是出版报纸。
  2. 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸。
  3. 当你不想再看报纸的时候,取消i订阅,他们就不会再送新报纸来。
  4. 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。

出版者+订阅者=观察者模式

了解报纸的订阅是怎么回事,就能知道观察者模式是怎么回事,只是名称不太一样;出版者改称为“主题”(Subject),订阅者改称为“观察者”(Observer).

  2.2 定义观察者模式

描述观察者模式,可以利用报纸订阅服务,以及出版者和订阅者做比较。

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

当一个对象改变状态,其他依赖者都会收到通知。

 主题和观察者定义了一对多的关系。观察者依赖于此主题,只要主题状态一有变化,观察者就会被通知。根据通知的风格,观察者可能因此新值而更新。

2.3 定义观察者模式:类图

 相关问答:

:这和一对多的关系有和关联

 答:利用观察者模式,主题是具有状态的对象.并且可以控制这些状态。也就是说,有“一个”具有状态的主题。另一方面、观察者使用这些状态、虽然这些状态并不属于他们。有许多的观察者、依赖主题来告诉他们状态何时改变了。这就产生一个关系:“一个”主题对“多个”观察者的关系。

问:其间的依赖是如何产生的?

答:因为主题是真正拥有数据的人,观察者是主题的依赖者,在数据变化时更新,这样比起让许多对象控制同一份数据来,可以得到更干净的OO设计

2.4 设计原则:松耦合

当两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此的细节。

观察者模式提供了一种对象设计,让主题和观察者之间松耦合

        任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何时候删除某些观察者
        有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所有实现了观察者接口的对象。
        我们可以独立地复用主题或观察者。如果我们在其他地方需要使用主题或观察者,可以轻易地复用,因为二者并非紧耦合。

        改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间的接口仍被遵守,我们就可以自由地改变他们。

松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
设计图

 3.代码实现

3.1 自行建立实现

首先建立一些接口:

public interface Subject {

    //这两个方法都需要一个观察者作为变量,该观察者是用来注册或者被删除的
    public void registerObserver(Observer o);

    public void removeObesrver(Observer o);

    //当主题状态改变时,这个方法会被调用,以通知所有的观察者
    public void notifyObservers();

}

// 所有的观察者都必须实现update()方法,以实现观察者接口。
//在这里,按照Mary和Sue的想法吧观测值传入观察者中
public interface Observer {
    //当气象观测值改变时,主题会把这些状态值当做方法的参数,传递给观察者
    public void update(float temp,float humidity,float pressure);
}

//只包含了一个方法display() 当布告板需要显示时,就调用此方法
public interface DisplayElement {

    public void display();
}

思考:

将观测值直接传入观察者中是更新状态的最直接的方法,但是在未来,这些观测值的种类和个数都可能改变,那么这些变化是否能够被很好地封装?或者是需要修改许多代码才能办到?

-------------------这个问题等到下面来解决

在完成接口的设置之后,在WeatherData中实现主题接口:

public class WeatherData implements Subject {
    private ArrayList observers;//用于记录观察者
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers=new ArrayList();
    }

    //当注册观察者时,只需要将其尾插到ArrayList中
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    //取消观察者的注册时,只需要将其从ArrayList中删除即可
    @Override
    public void removeObesrver(Observer o) {
        int i=observers.indexOf(o);
        if (i>0){
            observers.remove(i);
        }
    }

   //将状态告诉每个观察者。因为贯彻着都实现了update(),所以知道如何通知他们
    @Override
    public void notifyObservers() {
        for (int i=0;i<observers.size();i++){
            Observer observer=(Observer)observers.get(i);
            observer.update(temperature,humidity,pressure);
        }
    }

    //当从气象站得到更新的观测值时,将会通知观察者
    public void measurementsChanged(){
        notifyObservers();
    }

    //用于模拟观测值改变之后的执行逻辑
    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature=temperature;
        this.humidity=humidity;
        this.pressure=pressure;
        measurementsChanged();
    }

    //otherfunction 
}

开始编写布告板的代码。Weather-O-Rama气象站订购了三个布告板:目前状况布告板、统计布告板和预测布告板。

以 目前状况布告板--订阅者:为例

public class CurrentConditionsDisplay implements Observer,DisplayElement{
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData=weatherData;
        weatherData.registerObserver(this);//对这个气象站进行注册-订阅
    }


    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temperature=temp;
        this.humidity=humidity;
        display();//当update被调用时,我们把温度和湿度保存起来,然后调用display()
    }

    //用于显示最近的温度和湿度
    @Override
    public void display() {
        System.out.println("Current conditions: "+temperature+
                "F degress and "+humidity+"% humidity");
    }
}

编写一个测试程序--启动气象站:

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData=new WeatherData();

        CurrentConditionsDisplay currentConditionsDisplay=new CurrentConditionsDisplay(weatherData);

        weatherData.setMeasurements(80,65,30.4f);
        weatherData.setMeasurements(82,70,29.2f);
        weatherData.setMeasurements(78,90,19.1f);
    }
}

运行结果:

在实际使用需求中,观察者可能只需要主题中的一部分数据,那么根据需求自取比主题推送全部数据要方便;但从另一个角度来说,如果观察者需要全部的数据,则需要不断调用getter()方法来一点点的获取。因此,这两种模式都需要具备,那么Java内置的观察模式就能够解决这个问题。

3.2 使用Java内置的观察者模式

Java API有内置的观察者模式。java.util包中包含最基本的Observer接口与Observable类,这和我们的Subject接口与Observable类使用上更方便,因为许多功能都已经事先准备好了。可以使用推(push)或拉(pull)的方式传送数据。

用Java.util.Observer和java.util.Observable来修改气象站OO的设计:

Java内置的观察者模式如何运作

 Java内置的观察者模式运作方式,与在气象站中的实现类似,但是有一些小差异。最明显的差异是WeatehrData(即主体)现在扩展自Observable类,并继承该类的一些添加、删除‘通知观察者的方法。

如何将对象变成观察者?

如图以前一样,实现观察者接口,然后调用任何Observable对象的addObserver()方法。不想再当观察者时,调用deleteObserver()方法即可。

可观察者如何送出通知?

首先,利用扩展java.util.Observable接口产生“可观察者”类,然后,需要两个步骤:

1.先调用setChanged()方法,标记状态已经改变的事实。

2.然后调用两种notifyObservers()方法中的一个:

notifyObservers()

notifyObservers(Object arg)//当通知时,此版本可以传送任何的数据对象给每一个观察者,此处是 data Object

观察者如何接收通知?

和之前一样,观察者实现了更新的方法,但是方法的签名不太一样。

/*parameter*/
//Observable o   主题本身当做第一个变量,好让观察者知道是哪个主题通知它的。
//这正是传入notifyObservers()的数据对象。如果没有则说明为空
update(Observable o,Object arg)

 如果想将数据“”(push)给观察者,则可以将数据当做数据对象传送给notifyObservers(arg)方法。否则,观察者就必须从可观察者对象中“”(pull)数据。如何拉数据?再做一遍气象站,则就能看到。

查看serChange()方法的执行逻辑?

setChange()方法用于标记状态已经改变的事实,好让ntifyObservers()知道当它被调用时应该更新观察者。如果调用notifyObservers()之前没有先调用setChanged(),观察者就“不会”被通知。来看Observable内部的执行伪代码:

这么做是有其必要性的。setChanged()方法可以让你在更新观察者时,有更多的弹性。以气象站测量为例,如果温度计读书变化每0.1°就会更新,则会导致通知过于频繁,那么我们希望变化0.5°以上才更新,则可以在温度差距到达半度时,调用setChanged(),进行有效更新。

这个功能虽然不经常用,但是把这个功能准备好,当需要的时候就可以马上使用。宗旨,调用setChanged(),以便通知开始运转。类似的方法有 clearChanged()方法,将changed状态设置回false。另外也有一个hasChanged()方法,用于告知changed标志的当前状态。

3.3 利用内置的支持重做气象站

首先,把WeatherData改成使用java.util.Observable。

public class WeaherData extends Observable {
    private  float temperature;
    private float humidity;
    private float pressure;

    //无需追踪观察者了,也不需要管理注册与删除,这个交给超类Observable来负责
    public WeaherData() {
    }

    public void measurementsChanged(){
        setChanged();//注意:我们没有调用notifyObservers()传送数据对象,这表示我们采用的做法是"拉
        //“推” 就是用Object o封装数据对象 然后传送
        //“拉” 则不使用传送数据对象
        notifyObservers();
    }

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


    //因为使用的是"拉"的做法,因此设置了get方法。
    //观察者利用这些方法取得WeatherData对象的状态
    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

然后,再重做CurrentConditionDisplay:

public class CurrentConditionDisplay implements Observer, DisplayElement {

    Observable observable;
    private float temperature;
    private float humidity;

    //构造器需要一个Observable当参数,并将CurrentConditionDisplay对象登记为观察者
    public CurrentConditionDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }



    //改变update()方法,增加Observable和数据对象作为参数
    //在update()中,先确定可观察者属于WeatherData类型,然后利用getter方法获取温度和湿度测量值,最后调用display()
    @Override
    public void update(Observable obs, Object arg) {
        if (obs instanceof WeaherData){
            WeaherData weaherData=(WeaherData) obs;
            this.temperature=weaherData.getTemperature();
            this.humidity=weaherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: "+temperature+" F degress and "+humidity+"% humdity");
    }
}

可知观察者这边也是通过“拉”的方式,使用getter()方法自行获取所需要的数据

使用上面的测试代码,可以正常运行。

 测试代码的结果和上面那个相同,但是书上说不一样。

书上是java.util.Observable实现的notifyObservers()方法,这导致通知观察者的次序不同于我们先前的次序。这是因为两方选择不同的方式实现的。

但是可以确定的是,如果我们的代码依赖这样的次序,就是错的原因是:一旦观察者/可观察者的实现有所改变,通知次序就会改变,那么就会产生错误的结果。(以气象站为例,里面有布告板显示当前参数,有布告板显示最大参数+最小参数,如果这两个显示次序改变,则结果可能就会出错)。这不是我们认为的松耦合。 

 3.4 Java.util.Observable的黑暗面

可观察者,即java.util.Observable是一个“类”,而不是一个“接口”,并且它甚至没有实现一个“接口”。因此需要注意一些事实:

Observable是一个类:

上面的原则说“多用组合,少用继承”。因此得知,这不是一件好事,那这会造成什么问题呢?

首先,因为Observable是一个“类”,那么必须设计一个类继承他。如果某类想同时具有Observable类和另一个超类的行为,由于Java不支持多继承,则陷入困境。这会限制Observable的复用潜力。

并且,因为没有Observable接口,因此无法建立自己的实现,和Java内置的Observer API搭配使用,也无法将java.util的实现换成另一套做法的实现。

Observable将关键的方法保护起来

看Observable API,会发现setChanged()方法被保护起来了(被定义为protected)。这代表,除非继承自Observable,否则将无法创建Observable实例并组合到自己的对象中来。这个设计违反了第二个设计原则:“多用组合,少用继承”。

怎么用?

根据自己的实际需求,要么自己实现,要么直接使用内部API。

4.第二章总结

OO基础:抽象

OO原则:

  1. 封装变化
  2. 多用组合,少用继承
  3. 针对接口编程,不针对实现编程
  4. 为交互对象之间的松耦合设计而努力

OO模式:

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

观察者模式的代表人物-----MVC。

设计原则:

1.找出程序中会变化的方面,然后将其和固定不变的方面相分离。

2.针对接口编程,不针对实现编程

3.多用组合,少用继承。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值