观察者模式及其应用
摘要
软件设计模式是软件开发中最为精华的一部分,它可以给困难问题带去指引,也可以延续软件的生命。本文将从软件设计模式的概念出发,介绍何为设计模式和设计原则,继而介绍观察者模式和其实际应用。
关键词:设计模式、观察者模式、解耦
引言
软件设计模式是一种经过验证的、可重复使用的解决方案,用于解决软件设计中常见的问题。它是基于面向对象编程的思想,通过提供一系列的模板来帮助开发人员解决软件设计中的常见问题。这些模板包括如何组织代码、如何处理数据、如何管理对象之间的关系等。软件设计模式旨在提高代码的可重用性、可维护性和可扩展性。
设计模式相关知识
软件设计模式发展至今已经是已经出现各式各样模式,用于解决软件设计过程中的种种问题,其中最为基础通用的则是 GoF 在其《设计模式》一书中提出的 23 种设计模式。这23种设计模式现今成为了设计模式的经典,也是最为基础的设计模式。
23 种经典的设计模式分为三类:创建型、结构型、行为型。其中他们的主要作用如下:
创建型设计模式主要解决“对象的创建”问题。
结构型设计模式主要解决“类或对象的组合或组装”问题。
行为型设计模式主要解决的是“类或对象之间的交互”问题。
这几种类型的设计模式,即使他们功能各异,解决的问题各有不同。但是它们是遵循着最基本的 7 种设计原则,继而演化成通用的模板代码的。如果是设计模式是解决问题的模板,那么设计原则则是解决问题的根本有效条约。这几种设计原则共有7条,分别如下:
单一职责原则(Single Responsibility Principle,SRP):一个类只应该有一个引起它变化的原因。
开放封闭原则(Open Closed Principle,OCP):一个软件实体应该对扩展开放,对修改关闭。
里氏替换原则(Liskov Substitution Principle,LSP):子类可以替换父类并且不会影响程序的正确性。
接口隔离原则(Interface Segregation Principle,ISP):不应该强迫客户端依赖于它们不使用的接口。
依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有最少的了解。
组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP):尽量使用对象组合,而不是继承来达到复用的目的。
这些设计原则都是为了提高软件的可维护性、可扩展性、可重用性和灵活性,同时也能够降低代码的复杂度和耦合度。解决这些根本性问题,也就是在顺势解决软件设计中对应场景的不同问题了。
而在软件开发的需求场景中,有这么一种情况,当需要实现一对多的依赖关系,且需要实现一个对象的状态变化能够通知其他对象进行相应的处理,而这些被通知的对象数量可能是不固定的时候,我们需要一种设计模式可以来解决这个问题。这个问题是类或对象中的交互问题,那么自然是需要一种行为型设计模式,而这种设计模式就是观察者模式。
观察者模式的核心思想是将观察者(也就是被通知的对象)与被观察者(也就是状态发生变化的对象)进行解耦,让它们相互独立地变化。这样当被观察者的状态发生变化时,它只需要通知它所维护的观察者列表中的对象即可,而不需要知道这些对象的具体实现细节。
观察者模式
定义
观察者模式(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.
翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。即定义中的”一“就是被观察者,定义中”多“就是被观察者。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。
如何形象的理解这个模式呢?可以从”订阅-发布模式“这个定义形象理解。现以一个订阅杂志的过程为例子,高三(8)班的同学需要订阅《作文通讯》这个杂志,所以每个人都去邮政报务员那里交钱填写信息进行登记订阅;而到了每月的中旬发版的日子,邮政都会把最新一期的《作文通讯》送到每一个订阅同学的手中。这个过程就是一个较为形象的观察者模式,其中行为和定义的对应如下:
- 同学们交钱登记订阅 –> 定义一对多依赖
- 每月中旬发版时 –> 一个对象(被观察者)状态改变时
- 杂志送到每一个订阅同学的手中 –> 所有依赖的对象(观察者)都会自动收到通知
结构
观察者模式的定义如图所示,其中包括两个接口,以及对接口的实现类,其具体解释如下:
抽象主题(Subject)角色:也叫抽象目标类(抽象被观察者),它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
具体主题(Concrete Subject)角色:也叫具体目标类(具体被观察者),它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
依照这个类结构图,我们可以进行如下的编码实现。
编码实现
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);
}
}
}
public class ConcreteObserverOne implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverOne is notified.");
}
}
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();
subject.registerObserver(new ConcreteObserverOne());
subject.registerObserver(new ConcreteObserverTwo());
subject.notifyObservers(new Message());
}
}
依照这个编码实现,我们可以简单的模拟观察者模式,代码中实现好观察者和被观察者后,只需要在被观察者中订阅注册观察者的信息,那么就可以实现对观察者的消息分发了。在真实的软件开发中,并不需要照搬上面的简单代码,且往往是更加复杂变化多样的。观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register 函数还可以叫作 attach,remove 函数还可以叫作 detach 等。
应用场景
观察者模式适用场景如下:
当一个对象的状态发生变化时,需要通知其他对象进行相应的处理。
当一个对象需要将自己的状态变化通知给其他多个对象,而且这些对象的数量不固定时。
当一个对象需要将自己的状态变化通知给其他对象,但是它并不知道这些对象的具体实现细节时。
当一个对象的状态变化会引起其他对象的联动,而且这些联动需要根据具体的情况动态地进行调整时。
当一个对象的状态变化需要触发一系列的业务流程时,而这些业务流程又需要根据具体的情况动态地进行调整时。
特别注意的是,对于第二点应用场景,其中提到的”这些对象的数量不固定时“这个条件是指观察者模式一定会遵循开闭原则——对拓展开放,对修改关闭,让该模式松耦合、可维护和可拓展。
观察者模式适用于需要实现一对多的依赖关系的场景,同时也适用于需要实现松耦合的系统设计的场景。实际应用中,观察者模式有着广泛的应用,比如 GUI 设计、事件驱动系统、消息队列系统等。
观察者模式的实际应用
本文中的上述代码只是简单的演示,其中的被观察者处对消息的分发是同步阻塞的,对于所有消息是在主线程中排队一个个发布的。这种方式显然不满足当下多数系统的实际需求。而观察者模式的更多应用可以是:异步非阻塞的实现方式、跨进程的实现方式等。
跨进程的实现方式
如果提供了发送用户注册信息的 RPC 接口,我们仍然可以在 notifyObservers() 函数中调用 RPC 接口来发送数据。但是,我们还有更加优雅、更加常用的一种实现方式,那就是基于消息队列(Message Queue,比如 ActiveMQ)来实现。
但是这也会有新的问题出现,我们必须引入一个新的系统(消息队列),其增加了维护成本。但其好处是解耦更加彻底,在原来的实现方式中,观察者需要注册到被观察者中,被观察者需要依次遍历观察者来发送消息。而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。
异步非阻塞的实现方式
我们假设一个实际的场景,我们需要开发一个 P2P 投资理财系统,用户注册成功之后,我们会给用户发放投资体验金和欢迎信件。那么在同步阻塞的情况下,其实现如下。其中如果需要添加新的观察者时,UserController 类的 register() 函数完全不需要修改,只需要再添加一个实现了 RegObserver 接口的类,并且通过 setRegObservers() 函数将它注册到 UserController 类中即可。而 UserController 类则替代了被观察者这一角色。
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;
}
}
若想将其改造成异步非阻塞的实现方式,有两种方法。其中一种是:在每个 handleRegSuccess() 函数中创建一个新的线程执行代码逻辑;另一种是:在 UserController 的 register() 函数中使用线程池来执行每个观察者的 handleRegSuccess() 函数。两种实现方式的具体代码如下所示:
// 第一种实现方式,其他类代码不变,就没有再重复罗列
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() 函数中,增加了这部分业务代码的维护成本。
EventBus框架
使用 EventBus 框架可以解决上述两种方式的弊端,且隐藏实现细节,降低开发难度。EventBus 翻译为“事件总线”,它提供了实现观察者模式的骨架代码。我们可以基于此框架,非常容易地在自己的业务场景中实现观察者模式,不需要从零开始开发。其中,Google Guava EventBus 就是一个比较著名的 EventBus 框架,它不仅仅支持异步非阻塞模式,同时也支持同步阻塞模式。
public class UserController {
private UserService userService; // 依赖注入
private EventBus eventBus;
private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
public UserController() {
//eventBus = new EventBus(); // 同步阻塞模式
eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE)); // 异步非阻塞模式
}
public void setRegObservers(List<Object> observers) {
for (Object observer : observers) {
eventBus.register(observer);
}
}
public Long register(String telephone, String password) {
//省略输入参数的校验代码
//省略userService.register()异常的try-catch代码
long userId = userService.register(telephone, password);
eventBus.post(userId);
return userId;
}
}
public class RegPromotionObserver {
private PromotionService promotionService; // 依赖注入
@Subscribe
public void handleRegSuccess(Long userId) {
promotionService.issueNewUserExperienceCash(userId);
}
}
public class RegNotificationObserver {
private NotificationService notificationService;
@Subscribe
public void handleRegSuccess(Long userId) {
notificationService.sendInboxMessage(userId, "...");
}
}
利用 EventBus 框架实现的观察者模式,跟从零开始编写的观察者模式相比,从大的流程上来说,实现思路大致一样,都需要定义 Observer,并且通过 register() 函数注册 Observer,也都需要通过调用某个函数(比如,EventBus 中的 post() 函数)来给 Observer 发送消息(在 EventBus 中消息被称作事件 event)。
但在实现细节方面,它们又有些区别。基于 EventBus,我们不需要定义 Observer 接口,任意类型的对象都可以注册到 EventBus 中,通过 @Subscribe 注解来标明类中哪个函数可以接收被观察者发送的消息。
EventBus框架原理解释
EventBus 通过 @Subscribe 注解来标明,某个函数能接收哪种类型的消息。然后将这些对应的信息放置到 Observer 注册表中。等待被观察者发送消息。
当 EventBus 调用 post() 方法发送消息的时候,就结合 Observer 注册表和Java的反射机制,将对应的消息发送到对应的可接收函数中。对于同步阻塞模式,EventBus 在一个线程内依次执行相应的函数。对于异步非阻塞模式,EventBus 通过一个线程池来执行相应的函数。
结束语
通过使用观察者模式,可以实现松耦合的系统设计,提高系统的可维护性和扩展性。在合适的情况下,使用观察者模式可以大大提高开发人员的开发效率以及减少后期升级维护的付出。
参考文献
《设计模式之美》——王争
Pree W. Design patterns for object-oriented software development[M]. ACM Press/Addison-Wesley Publishing Co., 1995.