观察者模式

98 阅读10分钟

1. 简介

观察者模式。根据应用场景的不同,观察者模式会对应不同的代码实现方式:有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式

1.1 原理及应用场景剖析

观察者模式(Observer Design Pattern)也被称为发布订阅模式(Publish-Subscribe Design Pattern)。在 GoF 的《设计模式》一书中,它的定义是这样的:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新(可同步也可异步)自己。

一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。

实际上,观察者模式是一个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式,我们先来看其中最经典的一种实现方式。这也是在讲到这种模式的时候,很多书籍或资料给出的最常见的实现方式。

图片

在观察者模式中有如下角色:

  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。

  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。

  • Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。

  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

// 抽象被观察者
public interface Subject {
  // 注册
  void registerObserver(Observer observer);
  // 移除
  void removeObserver(Observer observer);
  // 通知
  void notifyObservers(Message message);
}

// 抽象观察者
public interface Observer {
  // 观察者更新
  void update(Message message);
}

// 具体被观察者
public class ConcreteSubject implements Subject {
  private List<Observer> observers = new ArrayList<Observer>();
  @Override
  public void registerObserver(Observer observer) {
    observers.add(observer);
  }
  @Override
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }
  @Override
  public void notifyObservers(Message message) {
    for (Observer observer : observers) {
      observer.update(message);
    }
  }
}

// 具体观察者1
public class ConcreteObserverOne implements Observer {
  @Override
  public void update(Message message) {
    //TODO: 获取消息通知,执行自己的逻辑...
    System.out.println("ConcreteObserverOne is notified.");
  }
}

// 具体观察者2
public class ConcreteObserverTwo implements Observer {
  @Override
  public void update(Message message) {
    //TODO: 获取消息通知,执行自己的逻辑...
    System.out.println("ConcreteObserverTwo is notified.");
  }
}

public class Demo {
  public static void main(String[] args) {
    // 具体被观察者
    ConcreteSubject subject = new ConcreteSubject();
    // 将具体观察者1注册到具体被观察者
    subject.registerObserver(new ConcreteObserverOne());
    // 将具体观察者2注册到具体被观察者
    subject.registerObserver(new ConcreteObserverTwo());
    // 通知
    subject.notifyObservers(new Message());
  }
}

上面的代码算是观察者模式的“模板代码”,只能反映大体的设计思路。

2. 一个Demo

2.1 天气预报功能要求

  1. 气象站可以将每天测量到的温度,湿度,气压等以公告的形式发布出去(比如发布到自己的网站或第三方)
  2. 需要设计开放型的API,便于其他第三方也能接入气象站获取数据局
  3. 提供温度,湿度和气压的接口
  4. 测量数据更新的时候,要能实时通知第三方

2.2 天气预报模块代码实现

类图

图片

package com.evan.observer;


/**
 * @Description
 * @ClassName Subject
 * @Author Evan
 * @date 2019.12.17 23:01
 */
public interface Subject {

    public void register(Observer observer);

    public void remove(Observer observer);

    public void nodifyObserver();
}
package com.evan.observer;

/**
 * @Description
 * @ClassName Observer
 * @Author Evan
 * @date 2019.12.17 23:02
 */
public interface Observer {

    public void update(float temperature,float preference, float humidity);

}
package com.evan.observer;

import java.util.ArrayList;

/**
 * @Description 观察者的核心类
 * 1. 包含最新的天气情况信息
 * 2. 含有 观察者集合,使用arrayList管理
 * 3. 当数据有更新时,就主动的调用arrayList,通知所有的订阅者
 * @ClassName WeatherData
 * @Author Evan
 * @date 2019.12.17 23:03
 */
public class WeatherData implements Subject {

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

    // 观察者集合
    private ArrayList<Observer> observers;


    public WeatherData() {
        observers = new ArrayList<Observer>();
    }


    public void dataChange() {
        // 调用 接入方update
        nodifyObserver();
    }

    // 当数据更新的时候调用setData
    public void setData(float temperature, float preference, float humidity) {
        this.temperature = temperature;
        this.preference = preference;
        this.humidity = humidity;
        // 调用dataChange 将数据信息推送给接入方
        dataChange();

    }

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

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


    // 遍历所有的观察者,并 通知
    @Override
    public void nodifyObserver() {

        for (int i = 0; i < observers.size(); i++) {
            observers.get(i).update(this.temperature, this.preference, this.humidity);
        }
    }
}
package com.evan.observer;

/**
 * @Description
 * @ClassName Sina
 * @Author Evan
 * @date 2019.12.17 23:05
 */
public class SinaSite implements Observer {

    private float temperature;
    private float preference;
    private float humidity;

    // 更新天气情况,是由WeatherData来调用,
    @Override
    public void update(float temperature, float preference, float humidity) {
        this.temperature = temperature;
        this.preference = preference;
        this.humidity = humidity;
        display();
    }

    public void display() {
        System.out.println("=========sina网站===========");
        System.out.println("=========sina网站气温"+this.temperature+"===========");
        System.out.println("=========sina网站气压"+this.preference+"===========");
        System.out.println("=========sina网站湿度"+this.humidity+"===========");
    }
}
package com.evan.observer;

/**
 * @Description
 * @ClassName Baidu
 * @Author Evan
 * @date 2019.12.17 23:04
 */
public class BaiduSite implements Observer {
    private float temperature;
    private float preference;
    private float humidity;

    // 更新天气情况,是由WeatherData来调用,
    @Override
    public void update(float temperature, float preference, float humidity) {
        this.temperature = temperature;
        this.preference = preference;
        this.humidity = humidity;
        display();
    }

    public void display() {
        System.out.println("=========百度网站===========");
        System.out.println("=========百度网站气温"+this.temperature+"===========");
        System.out.println("=========百度网站气压"+this.preference+"===========");
        System.out.println("=========百度网站湿度"+this.humidity+"===========");
    }
}
package com.evan.observer;

public class Client {

    public static void main(String[] args) {

        // 创建一个WeatherData
        WeatherData weatherData = new WeatherData();

        // 创建观察者
        BaiduSite baiduSite = new BaiduSite();
        SinaSite sinaSite = new SinaSite();

        // 注册到weatherData
        weatherData.register(baiduSite);
        weatherData.register(sinaSite);


        // 测试
        System.out.println("通知注册的各个观察者");
        weatherData.setData(10f, 100f, 30.3f);


        // 注销掉sina
        weatherData.remove(sinaSite);

        // 测试
        System.out.println("通知各个注册的观察者");
        weatherData.setData(20f, 10f, 10f);


    }
}

3 一个Demo

3.1 同步阻塞方式

假设我们在开发一个 P2P 投资理财系统,用户注册成功之后,我们会给用户发放投资体验金。代码实现大致是下面这个样子的:

public class UserController {
  private UserService userService; // 依赖注入
  private PromotionService promotionService; // 依赖注入
  public Long register(String telephone, String password) {
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);
    promotionService.issueNewUserExperienceCash(userId);
    return userId;
  }
}

虽然注册接口做了两件事情,注册和发放体验金,违反单一职责原则,但是,如果没有扩展和修改的需求,现在的代码实现是可以接受的。如果非得用观察者模式,就需要引入更多的类和更加复杂的代码结构,反倒是一种过度设计。

相反,如果需求频繁变动,比如,用户注册成功之后,不再发放体验金,而是改为发放优惠券,并且还要给用户发送一封“欢迎注册成功”的站内信。这种情况下,我们就需要频繁地修改 register() 函数中的代码,违反开闭原则。而且,如果注册成功之后需要执行的后续操作越来越多,那 register() 函数的逻辑会变得越来越复杂,也就影响到代码的可读性和可维护性。

这个时候,观察者模式就能派上用场了。利用观察者模式,我对上面的代码进行了重构。重构之后的代码如下所示:

public interface RegObserver {
  void handleRegSuccess(long userId);
}

public class RegPromotionObserver implements RegObserver {
  private PromotionService promotionService; // 依赖注入
  @Override
  public void handleRegSuccess(long userId) {
    promotionService.issueNewUserExperienceCash(userId);
  }
}

public class RegNotificationObserver implements RegObserver {
  private NotificationService notificationService;
  @Override
  public void handleRegSuccess(long userId) {
    notificationService.sendInboxMessage(userId, "Welcome...");
  }
}


public class UserController {
  private UserService userService; // 依赖注入
  private List<RegObserver> regObservers = new ArrayList<>();
  // 一次性设置好,之后也不可能动态的修改
  public void setRegObservers(List<RegObserver> observers) {
    regObservers.addAll(observers);
  }
  public Long register(String telephone, String password) {
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);
    for (RegObserver observer : regObservers) {
      observer.handleRegSuccess(userId);
    }
    return userId;
  }
}

当我们需要添加新的观察者的时候,比如,用户注册成功之后,推送用户注册信息给大数据征信系统,基于观察者模式的代码实现,UserController 类的 register() 函数完全不需要修改,只需要再添加一个实现了 RegObserver 接口的类,并且通过 setRegObservers() 函数将它注册到 UserController 类中即可。

当我们把发送体验金替换为发送优惠券的时候,需要修改 RegPromotionObserver 类中 handleRegSuccess() 函数的代码,这还是违反开闭原则呀?你说得没错,不过,相对于 register() 函数来说,handleRegSuccess() 函数的逻辑要简单很多,修改更不容易出错,引入 bug 的风险更低。

3.2 异步非阻塞的实现方式

3.2.1 原理与分析

上文中的实现方式,它是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子,register() 函数依次调用执行每个观察者的 handleRegSuccess() 函数,等到都执行完成之后,才会返回结果给客户端。

如果注册接口是一个调用比较频繁的接口,对性能非常敏感,希望接口的响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式,以此来减少响应时间。具体来讲,当 userService.register() 函数执行完成之后,我们启动一个新的线程来执行观察者的 handleRegSuccess() 函数,这样 userController.register() 函数就不需要等到所有的 handleRegSuccess() 函数都执行完成之后才返回结果给客户端。userController.register() 函数从执行 3 个 SQL 语句才返回,减少到只需要执行 1 个 SQL 语句就返回,响应时间粗略来讲减少为原来的 1/3。

如何实现一个异步非阻塞的观察者模式呢?简单一点的做法是,在每个 handleRegSuccess() 函数中,创建一个新的线程执行代码

3.2.2 简易实现

// 第一种实现方式,其他类代码不变,就没有再重复罗列
public class RegPromotionObserver implements RegObserver {
  private PromotionService promotionService; // 依赖注入
  @Override
  public void handleRegSuccess(long userId) {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        promotionService.issueNewUserExperienceCash(userId);
      }
    });
    thread.start();
  }
}


// 第二种实现方式,其他类代码不变,就没有再重复罗列
public class UserController {
  private UserService userService; // 依赖注入
  private List<RegObserver> regObservers = new ArrayList<>();
  private Executor executor;
  public UserController(Executor executor) {
    this.executor = executor;
  }
  public void setRegObservers(List<RegObserver> observers) {
    regObservers.addAll(observers);
  }
    
  public Long register(String telephone, String password) {
    //省略输入参数的校验代码
    //省略userService.register()异常的try-catch代码
    long userId = userService.register(telephone, password);
    for (RegObserver observer : regObservers) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          observer.handleRegSuccess(userId);
        }
      });
    }
    return userId;
  }
}

  • 第一种实现方式,频繁地创建和销毁线程比较耗时,并且并发线程数无法控制,创建过多的线程会导致堆栈溢出。
  • 第二种实现方式,尽管利用了线程池解决了第一种实现方式的问题,但线程池、异步执行逻辑都耦合在了 register() 函数中,增加了这部分业务代码的维护成本。

这种方式在开发kafka消费的时候经常使用


final ThreadFactory threadFactory = new ThreadFactoryBuilder()
        .setNameFormat("-%d")
        .setDaemon(true)
        .build();

final ExecutorService executorService = new ThreadPoolExecutor(1, 10,
        1000L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>(100), threadFactory);
        
      
private void dataSyncBatchDebug(BatchDebugProcessRequest batchDebugProcessRequest) {
    //build BatchDebugProcessor
    final BatchDebugProcessor batchDebugProcessor = this.debugTransform.batchDebugTransform(batchDebugProcessRequest.getTopic(),
            batchDebugProcessRequest.getPropertyId(),
            batchDebugProcessRequest.getIsMaserati(),
            redisService.getRedisTemplate());
    //async execute
    executorService.execute(() -> doDataSyncBatchDebug(batchDebugProcessRequest, batchDebugProcessor));
}

private void doDataSyncBatchDebug(BatchDebugProcessRequest batchDebugProcessRequest, BatchDebugProcessor batchDebugProcessor) {
    try (KafkaTimeStampConsumer kafkaTimeStampConsumer = new KafkaTimeStampConsumer(
            batchDebugProcessRequest.getTopic(),
            batchDebugProcessRequest.getBeginTime(),
            batchDebugProcessRequest.getCluster())) {
        int messageLimit = batchDebugProcessRequest.getMessageLimit();
        final Long endTime = batchDebugProcessRequest.getEndTime();
        final String uuid = batchDebugProcessor.getUuid();
        long count = 0;
        boolean isFinish = false;
        while (true) {
            final ConsumerRecords<String, String> records = kafkaTimeStampConsumer.consume();
            if (records.isEmpty()) {
                break;
            }
            //转换成List
            final ArrayList<String> valueList = new ArrayList<>(records.count());
            for (ConsumerRecord<String, String> record : records) {
                if (record.timestamp() > endTime || ++count >= messageLimit) {
                    isFinish = true;
                    break;
                }
                valueList.add(record.value());
            }
            //执行批量debug,对每条debug结果做日志打印
            boolean processSuccess = batchDebugProcessor.process(valueList, (debugResult) -> {
                Integer index = BatchDebugUtil.DEBUG_SUCCESS_DEFAULT_INDEX;
                if (Objects.nonNull(debugResult.getFilteredMessage())) {
                    index = debugResult.getFilteredMessage().getFilterIndex();
                }
                BatchDebugUtil.writeSingleLog(uuid, JSON.toJSONString(debugResult), index);
            });

            if (isFinish || !processSuccess) {
                break;
            }
            TimeUnit.SECONDS.sleep(1);
        }
    } finally {
        batchDebugProcessor.close();
    }
}



3.3 进程间的实现方式

上文中的不管是同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式。

如果用户注册成功之后,我们需要发送用户信息给大数据征信系统,而大数据征信系统是一个独立的系统,跟它之间的交互是跨不同进程的,那如何实现一个跨进程的观察者模式呢?

我们可以基于消息队列(Message Queue,比如 ActiveMQ)来实现。

当然,这种实现方式也有弊端,那就是需要引入一个新的系统(消息队列),增加了维护成本。不过,它的好处也非常明显。在原来的实现方式中,观察者需要注册到被观察者中,被观察者需要依次遍历观察者来发送消息。而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。

3.4 三种方式的比较

同步阻塞是最经典的实现方式,主要是为了代码解耦;异步非阻塞除了能实现代码解耦之外,还能提高代码的执行效率;进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。

4 观察者模式的总结

4.1 意图

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

4.2 观察者使用场景

  • 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
  • 何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
  • 如何解决:使用面向对象技术,可以将这种依赖关系弱化。

注意事项:

  1. 避免循环引用。
  2. 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式

4.3 观察者的优缺点

优点:

  1. 观察者和被观察者是抽象耦合的。

  2. 建立一套触发机制。

缺点:

  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。

  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。