首页 > 技术文章 > 模式的秘密——观察者模式

firstdream 2017-07-09 14:54 原文

模式的秘密——观察者模式

一、   观察者模式的定义

定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

二、   认识观察者模式

1、       目标与观察者之间的关系

目标与观察者之间是一对多的关系,当然观察者只有一个也是合理的。

2、       单向依赖

观察者和目标是单向依赖的关系,只有观察者依赖目标,而不是目标依赖观察者,只有目标知道什么时候通知观察者,整个过程中观察者都是被动的,被动的等待目标的通知。

3、       命名建议

第一,目标接口的定义,建议在名称后面跟上Subject;

第二,观察者接口的定义,建议在名称后面跟上Observer;

第三,观察者接口的更新方法,建议名称为update,参数可以根据需要自己定义。

4、       触发通知的时机

一般情况是在完成状态维护后触发通知,因为通知会传递数据,不能够先通知再改变数据。

5、       观察者模式的调用顺序示意图

6、       通知的顺序

多个观察者之间的通知顺序是不确定的,彼此之间是平行的,观察者之间不应该有相互依赖的关系。

三、   观察者模式的实例

我们以天气状况为例,不同的观察者会根据天气情况选择做不同的事情。

1、       首先,我们需要定义观察者接口,Observer.java

/*

 * 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象

 */

public interface Observer{

   /*

    * 更新的接口

    * @param subject 传入目标对象,方便获取相应的目标对象的状态

    */

   public voidupdate(WeatherSubject subject);

}

 

2、       接下来,我们定义观察者的构造类,并实现观察者接口

/*

 * 具体的观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致

 */

 

public class ConcreteObserverimplements Observer {

   // 观察者的名字

   private String observerName;

   // 天气内容的情况,这个消息从目标处获取

   private String weatherContent;

 

   // 提醒的内容

   private String remindThing;

 

   public String getObserverName() {

      return observerName;

   }

 

   public voidsetObserverName(String observerName) {

      this.observerName = observerName;

   }

 

   public String getWeatherContent() {

      return weatherContent;

   }

 

   public voidsetWeatherContent(String weatherContent) {

      this.weatherContent = weatherContent;

   }

 

   public String getRemindThing() {

      return remindThing;

   }

 

   public voidsetRemindThing(String remindThing) {

      this.remindThing = remindThing;

   }

 

   /*

    * 获取目标类的状态同步到观察者的状态中 (non-Javadoc)

    *

    * @see Observer#update(Subject)

    */

   @Override

   public voidupdate(WeatherSubject subject) {

      weatherContent = ((ConcreteWeatherSubject) subject).getWeatherContent();

      System.out.println(observerName + "收到了" + weatherContent + "," + remindThing);

 

   }

}

 

3、       然后,我们需要定义目标对象,我们以天气作为目标对象

import java.util.ArrayList;

import java.util.List;

 

/*

 * 目标对象,它知道观察它的观察者,并提供注册(添加)和删除观察者的接口

 */

public class WeatherSubject {

       // 用来保存注册的观察者对象

       privateList<Observer> observers = new ArrayList<>();

 

       // attachdetach notifyObservers

       /*

        * 把订阅天气的人添加到订阅者列表中

        */

       publicvoid attach(Observer observer) {

              observers.add(observer);

       }

       /*

        * 删除集合中的指定订阅天气的人

        */

       publicvoid detach(Observer observer) {

              observers.remove(observer);

       }

       /*

        * 通知所有注册的观察者对象

        */

       protectedvoid  notifyObservers() {

              for(Observerobserver: observers){

                     observer.update(this);

              }

       }

}

 

4、       接着就是目标对象的构造类,并继承自目标对象

/*

 * 具体的目标对象,负责把有关状态存入到相应的观察者对象中

 */

public classConcreteWeatherSubject extendsWeatherSubject {

   // 获取天气的内容信息

   private String weatherContent;

 

   public String getWeatherContent() {

      return weatherContent;

   }

 

   public voidsetWeatherContent(String weatherContent) {

      this.weatherContent = weatherContent;

      this.notifyObservers();

   }

 

}

 

5、       最后,我们来编写客户端测试一下效果

 

 

public class Client {

 

   public static void main(String[] args) {

 

      // 1、创建目标

      ConcreteWeatherSubjectweather = new ConcreteWeatherSubject();

 

      // 2、创建观察者

      ConcreteObserverobserverOne = new ConcreteObserver();

      observerOne.setObserverName("观察者1");

      observerOne.setRemindThing("适合出行旅游");

 

      ConcreteObserverobserverTwo = new ConcreteObserver();

      observerTwo.setObserverName("观察者2");

      observerTwo.setRemindThing("适合逛街购物");

 

      // 3、注册观察者

      weather.attach(observerOne);

      weather.attach(observerTwo);

 

      // 4、目标发布天气

      weather.setWeatherContent("明天天气晴朗,气温25度");

   }

 

}

 

6、       结果

四、   观察者模式的两种方式

观察者模式的实现有两种方式:推模型和拉模型。

推模型:目标对象主动向观察者推送目标的详细信息,推送的信息通常是目标对象的全部或部分数据。

拉模型:目标对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动向目标对象中获取,相当于是观察者从目标对象中拉取数据。一般这种模型的实现中,会把目标对象自身通过update方法传递给观察者。

上面的实例,我们就使用的是拉模型,接下来我们对上面的实例进行小小的修改,实现推模型。

1、首先,我们需要将Observer.java中的update方法进行修改,如下所示:

public void update(String content);

我们将update的参数由WeatherSubject修改为String。

2、然后,我们将ConcreteObserver.java中继承的update方法进行修改,如下所示:

@Override

   public void update(String content) {

      weatherContent = content;

      System.out.println(observerName + "收到了" + weatherContent + "," + remindThing);

   }

3、接着,我们把WeatherSubject.java中的notifyObservers方法进行适当的修改,如下所示:

protected void  notifyObservers(String content) {

      for(Observer observer: observers){

         observer.update(content);

      }

   }

4、最后,我们还需要将ConcreteWeatherSubject.java中的 setWeatherContent方法进行修改,如下所示:

public voidsetWeatherContent(String weatherContent) {

      this.weatherContent = weatherContent;

      this.notifyObservers(weatherContent);

   }

5、我们来运行一下,看看结果

推模型和拉模型的比较:

推模型是假定目标对象知道观察者需要的数据。推模型会使观察者对象难以复用。

拉模型是目标对象不知道观察者具体需要什么数据,因此把自身传给观察者,由观察者来取值。拉模型下,update方法的参数是目标对象本身,基本上可以适应各种情况的需要。

五、   利用Java提供的观察者实现

在java.util包中提供了Observable类,还有一个接口Observer提供了update()方法

使用Java提供的方法有以下四点好处:

第一,不需要再定义观察者和目标的接口了,JDK帮忙定义了

第二,具体的目标实现里面不需要再维护观察者的注册信息了,这个在Java中的Observable类里面已经帮忙实现好了。

第三,触发通知的方式有一点变化,要先调用setChanged方法,这个是Java为了帮助实现更精确的触发控制而提供的功能。

第四,具体观察者的实现里面,update方法其实能同时支持推模型和拉模型,这个是Java在定义的时候,就已经考虑进去了。

六、   使用Java提供的接口实现观察者模式

1、       定义目标对象类,并且继承Java中的Observable类

/*

 * 目标天气的具体实现类

 */

public classConcreteWeatherSubject extendsObservable {

   // 天气的内容

   private String content;

 

   public String getContent() {

      return content;

   }

 

   public voidsetContent(String content) {

      this.content = content;

      //使用Java中的Observer模式的时候,在通知之前,需要先调用setChanged()方法

      this.setChanged();

      /*

       * 在调用通知方法的时候,是选择推模型还是拉模型就需要根据具体情况确定了

       *this.notifyObservers();拉模型

       *this.notifyObservers(content); 推模型

       */

      this.notifyObservers(content);

     

   }

 

}

2、       定义观察者对象,并实现Observer接口

 

//具体的观察者对象

public class ConcreteObserverimplements Observer {

//观察者名称变量

   private String observerName;

  

   /*

    * (non-Javadoc)

    * @seejava.util.Observer#update(java.util.Observable, java.lang.Object)

    * 第一个参数传递的目标应用,采用的是拉的方式

    * 第二个参数推送的内容,采用的是推送的方式

    */

   @Override

   public voidupdate(Observable o, Object arg) {

      // TODO Auto-generated method stub

      //推方式

      System.out.println(observerName+"收到了推送消息:"+arg);

      //拉方式

      System.out.println(observerName+"拉取了消息:"+((ConcreteWeatherSubject)o).getContent());

   }

 

   public String getObserverName() {

      return observerName;

   }

 

   public voidsetObserverName(String observerName) {

      this.observerName = observerName;

   }

  

 

}

3、       编写测试方法

 

 

public class Client {

 

   public static void main(String[] args) {

      // TODO Auto-generated method stub

      //创建天气目标

      ConcreteWeatherSubjectweatherSubject = new ConcreteWeatherSubject();

      //创建观察者

      ConcreteObserverobserver1 = new ConcreteObserver();

      observer1.setObserverName("观察者1");

     

      ConcreteObserverobserver2 = new ConcreteObserver();

      observer2.setObserverName("观察者2");

     

      //注册观察者

      weatherSubject.addObserver(observer1);

      weatherSubject.addObserver(observer2);

      //目标更新天气情况

      weatherSubject.setContent("天气晴,气温30度");

   }

 

}

4、       运行结果

七、   观察者模式的优缺点

1、       观察者的优点

第一,观察者模式实现了观察者和目标之间的抽象耦合;

原本目标对象在状态发生改变的时候需要直接调用所有的观察者对象,但是抽象出观察者接口之后,目标和观察者之间就只是抽象层面上的耦合,目标只是知道观察者的接口,并不知道观察者的类,从而实现了目标与具体的观察者之间的解耦。

第二,观察者模式实现了动态联动;

由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态的控制注册的观察者来控制某个动作的联动范围,从而实现动态联动。

第三,观察者模式支持广播通信。

目标发送通知给观察者是面向所有注册的观察者,所以目标每次通知的信息就要对所有注册的观察者进行广播,也可以在目标上添加新的方法来限制广播的范围。

2、       观察者的缺点

第一,可能会引起无谓的操作;

由于观察者模式每次都是广播通信,不管需不需要每个观察者都会被调用update方法。如果观察者不需要执行相应的方法,调用了方法导致误更新那就麻烦了。

八、   观察者模式的使用场景

第一,当一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态改变;

第二,如果在更改一个对象的时候,需要同时连带改变其他的对象,而且不知道究竟应该有多少对象需要被连带改变;

第三,当一个对象必须通知其他的对象,但是你又希望这个对象和其他被通知的对象是松散耦合的。

九、   区别对待观察者场景

在使用观察者模式解决实际问题的时候,不同的观察者可能对于同一个目标关注的内容并不相同,比如:同样是天气信息,观察者1只关注下雨的天气情况,而观察者2只关注下雨和下雪的天气情况,那么怎么处理这样的场景呢,我们只有区别对待观察者场景问题,通过下面的例子我们来看看,如何处理上述场景的问题。

1、       定义观察者接口

//定义一个更新的接口方法给那些在目标发生改变的时候被通知的观察者对象调用

public interface Observer{

   // 更新的接口

   public voidupdate(WeatherSubject subject);

 

   // 设置观察者名称

   public voidsetObserverName(String observerName);

 

   // 获取观察者名称

   public String getObserverName();

}

2、       定义目标对象的抽象类

 

public abstract class WeatherSubject {

   // 用来保存注册的观察者对象

   public List<Observer> observers = new ArrayList<>();

 

   // 把观察者添加到列表中

   public void attach(Observer observer) {

      observers.add(observer);

   }

 

   // 删除集合中的观察者

   public void detach(Observer observer) {

      observers.remove(observer);

   }

 

   protected abstract voidnotifyObservers();

}

3、       定义目标对象的构造类

public classConcreteWeahterSubject extendsWeatherSubject {

   // “晴天” “下雨” “下雪”

   // 目标对象的状态

   private String weatherContent;

 

   @Override

   protected void notifyObservers() {

      // TODO Auto-generated method stub

      // 循环所有注册的观察者

      for (Observer observer : observers) {

         /*

          * 观察者1 需要“下雨”的条件通知,其他条件不通知

          * 观察者2 需要“下雨”或者“下雪”的条件通知,其他条件不通知

          * 如果是晴天不通知

          */

         // 下雨

         if ("下雨".equals(this.getWeatherContent())) {

            if ("观察者1".equals(observer.getObserverName())) {

               observer.update(this);

            }

            if ("观察者2".equals(observer.getObserverName())) {

               observer.update(this);

            }

         }

         // 下雪

         if ("下雪".equals(this.getWeatherContent())) {

            if ("观察者2".equals(observer.getObserverName())) {

               observer.update(this);

            }

         }

      }

   }

 

   public String getWeatherContent() {

      return weatherContent;

   }

 

   public voidsetWeatherContent(String weatherContent) {

      this.weatherContent = weatherContent;

      this.notifyObservers();

   }

 

}

4、       定义观察者的构造类

public class ConcreteObserverimplements Observer {

   // 观察者的名字

   private String observerName;

 

   // 天气内容

   private String weatherContent;

   // 提醒内容

   private String remindContent;

 

   @Override

   public voidupdate(WeatherSubject subject) {

      // TODO Auto-generated method stub

      weatherContent = ((ConcreteWeahterSubject) subject).getWeatherContent();

      System.out.println(observerName + "收到了:" + weatherContent + ", " + remindContent);

   }

 

   @Override

   public voidsetObserverName(String observerName) {

      this.observerName = observerName;

   }

 

   @Override

   public String getObserverName() {

      return observerName;

   }

 

   public String getWeatherContent() {

      return weatherContent;

   }

 

   public voidsetWeatherContent(String weatherContent) {

      this.weatherContent = weatherContent;

   }

 

   public String getRemindContent() {

      return remindContent;

   }

 

   public voidsetRemindContent(String remindContent) {

      this.remindContent = remindContent;

   }

 

}

5、       验证

public class Client {

 

   public static void main(String[] args) {

      // TODO Auto-generated method stub

      // 创建目标

      ConcreteWeahterSubjectweahterSubject = new ConcreteWeahterSubject();

 

      // 创建观察者

      ConcreteObserverobserver1 = new ConcreteObserver();

      observer1.setObserverName("观察者1");

      observer1.setRemindContent("下雨了,今天不适合旅游");

 

      ConcreteObserverobserver2 = new ConcreteObserver();

      observer2.setObserverName("观察者2");

      observer2.setRemindContent("下雨了,所有室外活动取消");

 

      // 注册观察者

      weahterSubject.attach(observer1);

      weahterSubject.attach(observer2);

 

      // 更新通知

      weahterSubject.setWeatherContent("下雨");

//    weahterSubject.setWeatherContent("下雪");

   }

 

}

 

6、       运行截图

       

以上就是观察者模式的基本内容,希望小伙伴们提出问题,我们共同探讨。

推荐阅读