7 观察者&模板模式


本文介绍行为型设计模式中常用的两种:观察者模式和模板模式

观察者模式(常用)

  1. 23 种经典的设计模式常被分为三类:创建型、结构型、行为型。前面已经学习了创建型和结构型,下面开始学习行为型设计模式。创建型设计模式主要解决“对象的创建”的问题,结构型设计模式主要解决“类或对象的组合或组装”的问题,行为型设计模式主要解决的就是“类或对象之间的交互”问题。
行为型设计模式比较多,有 11 个,几乎占了 23 种经典设计模式的一半。它们分别是:观察者模式、模板模式、策略模式、职责链模式、状态模式、迭代器模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
  1. 下面学习的第一个行为型设计模式,也是在实际的开发中用得比较多的一种模式:观察者模式。根据应用场景的不同,观察者模式会对应不同的代码实现方式:有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。以下重点讲解原理、实现、应用场景。紧接着实现一个基于观察者模式的异步非阻塞的 EventBus,加深对这个模式的理解。

Demo案例 - 天气预报

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

方案一

/**
 * 类是核心
 * 1. 包含最新的天气情况信息 
 * 2. 含有 CurrentConditions 对象
 * 3. 当数据有更新时,就主动的调用CurrentConditions对象update方法(含 display), 这样他们(接入方)就看到最新的信息
 *
 */
public class WeatherData {
	private float temperatrue;
	private float pressure;
	private float humidity;
    // 加入新的第三方
	private CurrentConditions currentConditions;
	
	public WeatherData(CurrentConditions currentConditions) {
		this.currentConditions = currentConditions;
	}

	public float getTemperature() {
		return temperatrue;
	}

	public float getPressure() {
		return pressure;
	}

	public float getHumidity() {
		return humidity;
	}
    
	public void dataChange() {
		// 调用接入方的 update
		currentConditions.update(getTemperature(), getPressure(), getHumidity());
	}

	// 当数据有更新时,就调用setData
	public void setData(float temperature, float pressure, float humidity) {
		this.temperatrue = temperature;
		this.pressure = pressure;
		this.humidity = humidity;
		// 调用dataChange,将最新的信息 推送给 接入方 currentConditions
		dataChange();
	}
}
// 显示当前天气情况(可以理解成是气象站自己的网站)
public class CurrentConditions {
	// 温度,气压,湿度
	private float temperature;
	private float pressure;
	private float humidity;

	// 更新天气情况,由WeatherData来调用,使用推送模式
	public void update(float temperature, float pressure, float humidity) {
		this.temperature = temperature;
		this.pressure = pressure;
		this.humidity = humidity;
		display();
	}

	//显示
	public void display() {
		System.out.println("***Today mTemperature: " + temperature + "***");
		System.out.println("***Today mPressure: " + pressure + "***");
		System.out.println("***Today mHumidity: " + humidity + "***");
	}
}
public class Client {
	public static void main(String[] args) {
		// 创建接入方currentConditions
		CurrentConditions currentConditions = new CurrentConditions();
		// 创建WeatherData并将接入方currentConditions传递到WeatherData中
		WeatherData weatherData = new WeatherData(currentConditions);

		// 更新天气情况
		weatherData.setData(30, 150, 40);

		// 天气情况变化
		System.out.println("============天气情况变化=============");
		weatherData.setData(40, 160, 20);
	}
}

问题分析

  1. 其他第三方接入气象站获取数据的问题,无法在运行时动态的添加第三方 (新浪网站)
  2. 违反ocp原则,当加入第三方时,需要修改WeatherData代码,不利于维护。

观察者模式方案

// 接口,让WeatherData来实现 
public interface Subject {
	void registerObserver(Observer o);
	void removeObserver(Observer o);
	void notifyObservers();
}
// 观察者接口
public interface Observer {
	void update(float temperature, float pressure, float humidity);
}
public class WeatherData implements Subject {
	private float temperature;
	private float pressure;
	private float humidity;
	// 观察者集合
	private ArrayList<Observer> observers;
	// 加入新的第三方
	public WeatherData() {
		observers = new ArrayList<>();
	}

	public float getTemperature() {
		return temperature;
	}

	public float getPressure() {
		return pressure;
	}

	public float getHumidity() {
		return humidity;
	}

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

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

	// 注册一个观察者
	@Override
	public void registerObserver(Observer o) {
		observers.add(o);
	}

	// 移除一个观察者
	@Override
	public void removeObserver(Observer o) {
		if(observers.contains(o)) {
			observers.remove(o);
		}
	}

	// 遍历所有的观察者,并通知
	@Override
	public void notifyObservers() {
		for(int i = 0; i < observers.size(); i++) {
			observers.get(i).update(this.temperature, this.pressure, this.humidity);
		}
	}
}
public class CurrentConditions implements Observer {
	// 温度,气压,湿度
	private float temperature;
	private float pressure;
	private float humidity;

	// 更新天气情况,由WeatherData调用,使用推送模式
    @Override
	public void update(float temperature, float pressure, float humidity) {
		this.temperature = temperature;
		this.pressure = pressure;
		this.humidity = humidity;
		display();
	}

	// 显示
	public void display() {
		System.out.println("***Today mTemperature: " + temperature + "***");
		System.out.println("***Today mPressure: " + pressure + "***");
		System.out.println("***Today mHumidity: " + humidity + "***");
	}
}
public class BaiduSite implements Observer {
	// 温度,气压,湿度
	private float temperature;
	private float pressure;
	private float humidity;

	// 更新天气情况,由WeatherData调用,使用推送模式
    @Override
	public void update(float temperature, float pressure, float humidity) {
		this.temperature = temperature;
		this.pressure = pressure;
		this.humidity = humidity;
		display();
	}

	// 显示
	public void display() {
		System.out.println("===百度网站====");
		System.out.println("***百度网站 气温 : " + temperature + "***");
		System.out.println("***百度网站 气压: " + pressure + "***");
		System.out.println("***百度网站 湿度: " + humidity + "***");
	}
}
public class Client {
	public static void main(String[] args) {
		// 创建一个WeatherData
		WeatherData weatherData = new WeatherData();

		// 创建观察者
		CurrentConditions currentConditions = new CurrentConditions();
		BaiduSite baiduSite = new BaiduSite();

		// 注册到weatherData
		weatherData.registerObserver(currentConditions);
		weatherData.registerObserver(baiduSite);

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

		// 测试
		System.out.println("通知各个注册的观察者, 看看信息");
		weatherData.setData(20f, 200f, 60.6f);
	}
}

输出如下:

通知各个注册的观察者, 看看信息
***Today mTemperature: 10.0***
***Today mPressure: 100.0***
***Today mHumidity: 30.3***
===百度网站====
***百度网站 气温 : 10.0***
***百度网站 气压: 100.0***
***百度网站 湿度: 30.3***
通知各个注册的观察者, 看看信息
***Today mTemperature: 20.0***
***Today mPressure: 200.0***
***Today mHumidity: 60.6***
===百度网站====
***百度网站 气温 : 20.0***
***百度网站 气压: 200.0***
***百度网站 湿度: 60.6***

原理及应用场景剖析

观察者模式也被称为发布订阅模式(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.

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

  1. 一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。
  2. 实际上,观察者模式是一个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式。现在先来看其中最经典的一种实现方式。这也是在讲到这种模式的时候,很多书籍或资料给出的最常见的实现方式。具体的代码如下所示:
import java.util.ArrayList;
import java.util.List;

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) {
		System.out.println("ConcreteObserverOne is notified.");
	}
}

public class ConcreteObserverTwo implements Observer {
	@Override
	public void update(Message message) {
		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());
	}
}
  1. 实际上,上面的代码算是观察者模式的“模板代码”,只能反映大体的设计思路。在真实的软件开发中,并不需要照搬上面的模板代码。观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register函数还可以叫作attachremove函数还可以叫作detach等等。不过,万变不离其宗,设计思路都是差不多的。
  2. 原理和代码实现都非常简单,也比较好理解。下面通过一个具体的例子来重点讲一下,什么情况下需要用到这种设计模式?或者说,这种设计模式能解决什么问题呢?
  3. 假设要开发一个 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;
	}
}
  1. 虽然注册接口做了两件事情,注册和发放体验金,违反单一职责原则,但是,如果没有扩展和修改的需求,现在的代码实现是可以接受的。如果非得用观察者模式,就需要引入更多的类和更加复杂的代码结构,反倒是一种过度设计。

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

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

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;
	}
}
  1. 需要添加新的观察者时,比如,用户注册成功之后,推送用户注册信息给大数据征信系统,基于观察者模式的代码实现,UserController类的register()方法完全不需要修改,只需要再添加一个实现了RegObserver接口的类,并且通过setRegObservers()函数将它注册到UserController类中即可。

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

  3. 前面已经学习了很多设计模式,可以发现,实际上设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。借助设计模式,利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。

基于不同应用场景的不同实现方式

  1. 观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。
  2. 不同的应用场景和需求下,这个模式也有截然不同的实现方式,开篇的时候提到,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
  3. 之前讲到的实现方式,从刚刚的分类方式上来看,是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子,register()方法依次调用执行每个观察者的handleRegSuccess()方法,等到都执行完成之后,才会返回结果给客户端。
  4. 如果注册接口是一个调用比较频繁的接口,对性能非常敏感,希望接口的响应时间尽可能短,那可以将同步阻塞的实现方式改为异步非阻塞的实现方式,以此来减少响应时间。具体来讲,当userService.register()函数执行完成之后,启动一个新的线程来执行观察者的handleRegSuccess()函数,这样userController.register()函数就不需要等到所有的handleRegSuccess()函数都执行完成之后才返回结果给客户端。userController.register()函数从执行 3 个 SQL 语句才返回,减少到只需要执行 1 个 SQL 语句就返回,响应时间粗略来讲减少为原来的 1/3。
  5. 那如何实现一个异步非阻塞的观察者模式呢?简单一点的做法是,在每个handleRegSuccess()函数中,创建一个新的线程执行代码。不过,还有更加优雅的实现方式,那就是基于EventBus来实现。在下一讲中,会用一节的时间,借鉴 Google Guava EventBus 框架的设计思想,开发一个支持异步非阻塞的EventBus框架。它可以复用在任何需要异步非阻塞观察者模式的应用场景中。
  6. 刚刚讲到的两个场景,不管是同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式。如果用户注册成功之后,需要发送用户信息给大数据征信系统,而大数据征信系统是一个独立的系统,跟它之间的交互是跨不同进程的,那如何实现一个跨进程的观察者模式呢?
  7. 如果大数据征信系统提供了发送用户注册信息的 RPC 接口,仍然可以沿用之前的实现思路,在handleRegSuccess()函数中调用 RPC 接口来发送数据。但是还有更加优雅、更加常用的一种实现方式,那就是基于消息队列(Message Queue,比如 ActiveMQ)来实现。
  8. 当然,这种实现方式也有弊端,那就是需要引入一个新的系统(消息队列),增加了维护成本。不过,它的好处也非常明显。在原来的实现方式中,观察者需要注册到被观察者中,被观察者需要依次遍历观察者来发送消息。而基于消息队列的实现方式,被观察者和观察者的解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。

如何实现一个异步非阻塞的EventBus框架?

  1. 之前学习了观察者模式的原理、实现、应用场景,重点介绍了不同应用场景下,几种不同的实现方式,包括:同步阻塞、异步非阻塞、进程内、进程间的实现方式。
  2. 同步阻塞是最经典的实现方式,主要是为了代码解耦;异步非阻塞除了能实现代码解耦之外,还能提高代码的执行效率;进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。
  3. 下面聚焦于异步非阻塞的观察者模式,实现一个类似 Google Guava EventBus 的通用框架。学完本节之后会发现,实现一个框架也并非一件难事。

异步非阻塞观察者模式的简易实现

  1. 上一节讲到,对于异步非阻塞观察者模式,如果只是实现一个简易版本,不考虑任何通用性、复用性,实际上是非常容易的。
  2. 有两种实现方式。其中一种是:在每个handleRegSuccess()函数中创建一个新的线程执行代码逻辑;另一种是:在 UserControllerregister()函数中使用线程池来执行每个观察者的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;
	}
}
  1. 对于第一种实现方式,频繁地创建和销毁线程比较耗时,并且并发线程数无法控制,创建过多的线程会导致堆栈溢出。第二种实现方式,尽管利用了线程池解决了第一种实现方式的问题,但线程池、异步执行逻辑都耦合在了 register()函数中,增加了这部分业务代码的维护成本。

  2. 如果需求更加极端一点,需要在同步阻塞和异步非阻塞之间灵活切换,那就要不停地修改UserController的代码。除此之外,如果在项目中,不止一个业务模块需要用到异步非阻塞观察者模式,那这样的代码实现也无法做到复用。

  3. 框架的作用有:隐藏实现细节、降低开发难度、代码复用,解耦业务与非业务代码,让程序员聚焦业务开发。针对异步非阻塞观察者模式,也可以将它抽象成框架来达到这样的效果,而这个框架就是接下来要讲的 EventBus。

EventBus 框架功能需求介绍

  1. EventBus 翻译为“事件总线”,它提供了实现观察者模式的骨架代码。我们可以基于此框架,非常容易地在自己的业务场景中实现观察者模式,不需要从零开始开发。其中,Google Guava EventBus 就是一个比较著名的 EventBus 框架,它不仅仅支持异步非阻塞模式,同时也支持同步阻塞模式
  2. 现在就通过例子来看一下,Guava EventBus 具有哪些功能。还是上节那个用户注册的例子,用 Guava 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, "...");
    }
}
  1. 利用 EventBus 框架实现的观察者模式,跟从零开始编写的观察者模式相比,从大的流程上来说,实现思路大致一样,都需要定义 Observer,并且通过 register() 函数注册 Observer,也都需要通过调用某个函数(比如,EventBus 中的 post() 函数)来给 Observer 发送消息(在 EventBus 中消息被称作事件 event)。

  2. 但在实现细节方面,它们又有些区别。基于 EventBus,我们不需要定义 Observer 接口,任意类型的对象都可以注册到 EventBus 中,通过 @Subscribe 注解来标明类中哪个函数可以接收被观察者发送的消息。

  3. 接下来,我们详细地讲一下,Guava EventBus 的几个主要的类和函数。

    • EventBus、AsyncEventBus

      • Guava EventBus 对外暴露的所有可调用接口,都封装在 EventBus 类中。其中,EventBus 实现了同步阻塞的观察者模式,AsyncEventBus 继承自 EventBus,提供了异步非阻塞的观察者模式。具体使用方式如下所示:
      EventBus eventBus = new EventBus(); // 同步阻塞模式
      EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8))// 异步阻塞模式
    • register() 函数

      • EventBus 类提供了 register() 函数用来注册观察者。具体的函数定义如下所示。它可以接受任何类型(Object)的观察者。而在经典的观察者模式的实现中,register() 函数必须接受实现了同一 Observer 接口的类对象。
      public void register(Object object);
    • unregister() 函数

      • 相对于 register() 函数,unregister() 函数用来从 EventBus 中删除某个观察者。我就不多解释了,具体的函数定义如下所示:
      public void unregister(Object object);
    • post() 函数

      • EventBus 类提供了 post() 函数,用来给观察者发送消息。具体的函数定义如下所示:
      public void post(Object event);
  4. 跟经典的观察者模式的不同之处在于,当我们调用 post() 函数发送消息的时候,并非把消息发送给所有的观察者,而是发送给可匹配的观察者。所谓可匹配指的是,能接收的消息类型是发送消息(post 函数定义中的 event)类型的父类。我举个例子来解释一下。

  5. 比如,AObserver 能接收的消息类型是 XMsg,BObserver 能接收的消息类型是 YMsg,CObserver 能接收的消息类型是 ZMsg。其中,XMsg 是 YMsg 的父类。当我们如下发送消息的时候,相应能接收到消息的可匹配观察者如下所示:

XMsg xMsg = new XMsg();

YMsg yMsg = new YMsg();

ZMsg zMsg = new ZMsg();

post(xMsg); => AObserver接收到消息

post(yMsg); => AObserverBObserver接收到消息

post(zMsg); => CObserver接收到消息
  1. 你可能会问,每个 Observer 能接收的消息类型是在哪里定义的呢?我们来看下 Guava EventBus 最特别的一个地方,那就是 @Subscribe 注解。

    • @Subscribe 注解

      • EventBus 通过 @Subscribe 注解来标明,某个函数能接收哪种类型的消息。具体的使用代码如下所示。在 DObserver 类中,我们通过 @Subscribe 注解了两个函数 f1()、f2()。
      public DObserver{
          //...省略其他属性和方法...
          @Subscribe
          public  void f1(PMsg event){ //... }
          
          @Subscribe
          public  void f2(QMsg event){ //... }
      }

当通过 register() 函数将 DObserver 类对象注册到 EventBus 的时候,EventBus 会根据 @Subscribe 注解找到 f1() 和 f2(),并且将两个函数能接收的消息类型记录下来(PMsg->f1,QMsg->f2)。当我们通过 post() 函数发送消息(比如 QMsg 消息)的时候,EventBus 会通过之前的记录(QMsg->f2),调用相应的函数(f2)。

手把手实现一个 EventBus 框架

  1. Guava EventBus 的功能我们已经讲清楚了,总体上来说,还是比较简单的。接下来,我们就重复造轮子,“山寨”一个 EventBus 出来。
  2. 我们重点来看,EventBus 中两个核心函数 register() 和 post() 的实现原理。弄懂了它们,基本上就弄懂了整个 EventBus 框架。下面两张图是这两个函数的实现原理图。
  1. 从图中我们可以看出,最关键的一个数据结构是 Observer 注册表,记录了消息类型和可接收消息函数的对应关系。当调用 register() 函数注册观察者的时候,EventBus 通过解析 @Subscribe 注解,生成 Observer 注册表。当调用 post() 函数发送消息的时候,EventBus 通过注册表找到相应的可接收消息的函数,然后通过 Java 的反射语法来动态地创建对象、执行函数。对于同步阻塞模式,EventBus 在一个线程内依次执行相应的函数。对于异步非阻塞模式,EventBus 通过一个线程池来执行相应的函数。

  2. 弄懂了原理,实现起来就简单多了。整个小框架的代码实现包括 5 个类:EventBus、AsyncEventBus、Subscribe、ObserverAction、ObserverRegistry。接下来,我们依次来看下这 5 个类。

Subscribe

Subscribe 是一个注解,用于标明观察者中的哪个函数可以接收消息。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {}
ObserverAction

ObserverAction 类用来表示 @Subscribe 注解的方法,其中,target 表示观察者类,method 表示方法。它主要用在 ObserverRegistry 观察者注册表中。

public class ObserverAction {
    private Object target;
    private Method method;

    public ObserverAction(Object target, Method method) {
        this.target = Preconditions.checkNotNull(target);
        this.method = method;
        this.method.setAccessible(true);
    }

    public void execute(Object event) { // event是method方法的参数
        try {
            method.invoke(target, event);
        } catch (InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
ObserverRegistry
  1. ObserverRegistry 类就是前面讲到的 Observer 注册表,是最复杂的一个类,框架中几乎所有的核心逻辑都在这个类中。这个类大量使用了 Java 的反射语法,不过代码整体来说都不难理解,其中,一个比较有技巧的地方是 CopyOnWriteArraySet 的使用。
  2. CopyOnWriteArraySet,顾名思义,在写入数据的时候,会创建一个新的 set,并且将原始数据 clone 到新的 set 中,在新的 set 中写入数据完成之后,再用新的 set 替换老的 set。这样就能保证在写入数据的时候,不影响数据的读取操作,以此来解决读写并发问题。除此之外,CopyOnWriteSet 还通过加锁的方式,避免了并发写冲突。具体的作用你可以去查看一下 CopyOnWriteSet 类的源码,一目了然。
public class ObserverRegistry {
    private ConcurrentMap<Class<?>, CopyOnWriteArraySet<ObserverAction>> registry = new ConcurrentHashMap<>();

    public void register(Object observer) {
        Map<Class<?>, Collection<ObserverAction>> observerActions = findAllObserverActions(observer);
        for (Map.Entry<Class<?>, Collection<ObserverAction>> entry : observerActions.entrySet()) {
            Class<?> eventType = entry.getKey();
            Collection<ObserverAction> eventActions = entry.getValue();
            CopyOnWriteArraySet<ObserverAction> registeredEventActions = registry.get(eventType);
            if (registeredEventActions == null) {
                registry.putIfAbsent(eventType, new CopyOnWriteArraySet<>());
                registeredEventActions = registry.get(eventType);
            }
            registeredEventActions.addAll(eventActions);
        }
    }

    public List<ObserverAction> getMatchedObserverActions(Object event) {
        List<ObserverAction> matchedObservers = new ArrayList<>();
        Class<?> postedEventType = event.getClass();

        for (Map.Entry<Class<?>, CopyOnWriteArraySet<ObserverAction>> entry : registry.entrySet()) {
            Class<?> eventType = entry.getKey();
            Collection<ObserverAction> eventActions = entry.getValue();
            if (postedEventType.isAssignableFrom(eventType)) {
                matchedObservers.addAll(eventActions);
            }
        }
        return matchedObservers;
    }

    private Map<Class<?>, Collection<ObserverAction>> findAllObserverActions(Object observer) {
        Map<Class<?>, Collection<ObserverAction>> observerActions = new HashMap<>();
        Class<?> clazz = observer.getClass();

        for (Method method : getAnnotatedMethods(clazz)) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            Class<?> eventType = parameterTypes[0];
            if (!observerActions.containsKey(eventType)) {
                observerActions.put(eventType, new ArrayList<>());
            }
            observerActions.get(eventType).add(new ObserverAction(observer, method));
        }
        return observerActions;
    }

    private List<Method> getAnnotatedMethods(Class<?> clazz) {
        List<Method> annotatedMethods = new ArrayList<>();
        
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Subscribe.class)) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                Preconditions.checkArgument(parameterTypes.length == 1,
                        "Method %s has @Subscribe annotation but has %s parameters."
                                + "Subscriber methods must have exactly 1 parameter.",
                        method, parameterTypes.length);
                annotatedMethods.add(method);
            }
        }
        return annotatedMethods;
    }
}
EventBus

EventBus 实现的是阻塞同步的观察者模式。看代码你可能会有些疑问,这明明就用到了线程池 Executor 啊。实际上,MoreExecutors.directExecutor() 是 Google Guava 提供的工具类,看似是多线程,实际上是单线程。之所以要这么实现,主要还是为了跟 AsyncEventBus 统一代码逻辑,做到代码复用。

public class EventBus {
    private Executor executor;
    private ObserverRegistry registry = new ObserverRegistry();

    public EventBus() {
        this(MoreExecutors.directExecutor());
    }

    protected EventBus(Executor executor) {
        this.executor = executor;
    }

    public void register(Object object) {
        registry.register(object);
    }

    public void post(Object event) {
        List<ObserverAction> observerActions = registry.getMatchedObserverActions(event);
        
        for (ObserverAction observerAction : observerActions) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    observerAction.execute(event);
                }
            });
        }
    }
}
AsyncEventBus

有了 EventBus,AsyncEventBus 的实现就非常简单了。为了实现异步非阻塞的观察者模式,它就不能再继续使用 MoreExecutors.directExecutor() 了,而是需要在构造函数中,由调用者注入线程池。

public class AsyncEventBus extends EventBus {
    
    public AsyncEventBus(Executor executor) {
        super(executor);
    }
}

至此,我们用了不到 200 行代码,就实现了一个还算凑活能用的 EventBus,从功能上来讲,它跟 Google Guava EventBus 几乎一样。不过,如果去查看Google Guava EventBus的源码,你会发现,在实现细节方面,相比我们现在的实现,它其实做了很多优化,比如优化了在注册表中查找消息可匹配函数的算法。如果有时间的话,建议你去读一下它的源码。

模板模式

我们再学习另外一种行为型设计模式,模板模式。我们多次强调,绝大部分设计模式的原理和实现,都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。模板模式也不例外。模板模式主要是用来解决复用和扩展两个问题。我们今天会结合 Java Servlet、JUnit TestCase、Java InputStream、Java AbstractList 四个例子来具体讲解这两个作用。

模板模式的原理与实现

  1. 模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
  1. 翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

  2. 这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

  3. 原理很简单,代码实现就更加简单,我写了一个示例代码,如下所示。templateMethod() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为 abstract,是为了强迫子类去实现。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现比较灵活,待会儿讲到应用场景的时候,我们会有具体的体现。

public abstract class AbstractClass {
    public final void templateMethod() {
        //...
        method1();
        //...
        method2();
        //...
    }

    protected abstract void method1();
    protected abstract void method2();
}

public class ConcreteClass1 extends AbstractClass {

    @Override
    protected void method1() {
        //...
    }

    @Override
    protected void method2() {
        //...
    }
}

public class ConcreteClass2 extends AbstractClass {

    @Override
    protected void method1() {
        //...
    }

    @Override
    protected void method2() {
        //...
    }
}

AbstractClass demo = ConcreteClass1();
demo.templateMethod();

Demo案例 - 制作豆浆

编写制作豆浆的程序,说明如下:

  1. 制作豆浆的流程 选材—>添加配料—>浸泡—>放到豆浆机打碎
  2. 通过添加不同的配料,可以制作出不同口味的豆浆
  3. 选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的
  4. 请使用 模板方法模式 完成 (说明:因为模板方法模式,比较简单,很容易就想到这个方案,因此就直接使用,

不再使用传统的方案来引出模板方法模式

//抽象类,表示豆浆
public abstract class SoyaMilk {

   //模板方法, make , 模板方法可以做成final , 不让子类去覆盖.
   final void make() {
      
      select(); 
      if(customerWantCondiments()) {
         addCondiments();
      }
      soak();
      beat();
      
   }
   
   //选材料
   void select() {
      System.out.println("第一步:选择好的新鲜黄豆  ");
   }
   
   //添加不同的配料, 抽象方法, 子类具体实现
   abstract void addCondiments();
   
   //浸泡
   void soak() {
      System.out.println("第三步, 黄豆和配料开始浸泡, 需要3小时 ");
   }
    
   void beat() {
      System.out.println("第四步:黄豆和配料放到豆浆机去打碎  ");
   }
   
   //钩子方法,决定是否需要添加配料
   boolean customerWantCondiments() {
      return true;
   }
}
public class RedBeanSoyaMilk extends SoyaMilk {

  @Override
  void addCondiments() {
    // TODO Auto-generated method stub
    System.out.println(" 加入上好的红豆 ");
  }
}
// 纯豆浆
public class PureSoyaMilk extends SoyaMilk {

  @Override
  void addCondiments() {
    // TODO Auto-generated method stub
    // 空实现
  }

  @Override
  boolean customerWantCondiments() {
    // TODO Auto-generated method stub
    return false;
  }
}
public class PeanutSoyaMilk extends SoyaMilk {

  @Override
  void addCondiments() {
    // TODO Auto-generated method stub
    System.out.println(" 加入上好的花生 ");
  }
}
public class Client {

  public static void main(String[] args) {
    System.out.println("----制作红豆豆浆----");
    SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk();
    redBeanSoyaMilk.make();

    System.out.println("----制作花生豆浆----");
    SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
    peanutSoyaMilk.make();

    System.out.println("----制作纯豆浆----");
    SoyaMilk pureSoyaMilk = new PureSoyaMilk();
    pureSoyaMilk.make();
  }
}

模板模式作用一:复用

开篇的时候,我们讲到模板模式有两大作用:复用和扩展。我们先来看它的第一个作用:复用。模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。所有的子类都可以复用父类中模板方法定义的流程代码。我们通过两个小例子来更直观地体会一下。

Java InputStream

  1. Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。我们拿 InputStream 来举例说明一下。
  2. 我把 InputStream 部分相关代码贴在了下面。在代码中,read() 函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个方法也被命名为了 read(),只是参数跟模板方法不同。
import java.io.IOException;

public abstract class InputStream implements Closeable {
    //...省略其他代码...

    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }
        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte) c;
        int i = 1;
        try {
            for (; i < len; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte) c;
            }
        } catch (IOException ee) {
        }
        return i;

    }
    public abstract int read() throws IOException;
}

public class ByteArrayInputStream extends InputStream {
    //...省略其他代码...

    @Override
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
}

Java AbstractList

在 Java AbstractList 类中,addAll() 函数可以看作模板方法,add() 是子类需要重写的方法,尽管没有声明为 abstract 的,但函数实现直接抛出了 UnsupportedOperationException 异常。前提是,如果子类不重写是不能使用的。

public boolean addAll(int index,Collection<?extends E> c){
        rangeCheckForAdd(index);
        boolean modified=false;

        for(E e:c){
            add(index++,e);
            modified=true;
        }
        return modified;
}

public void add(int index,E element){
        throw new UnsupportedOperationException();
}

模板模式作用二:扩展

模板模式的第二大作用的是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。我们通过 Junit TestCase、Java Servlet 两个例子来解释一下。

Java Servlet

对于 Java Web 项目开发来说,常用的开发框架是 SpringMVC。利用它,我们只需要关注业务代码的编写,底层的原理几乎不会涉及。但是,如果我们抛开这些高级框架来开发 Web 项目,必然会用到 Servlet。实际上,使用比较底层的 Servlet 来开发 Web 项目也不难。我们只需要定义一个继承 HttpServlet 的类,并且重写其中的 doGet() 或 doPost() 方法,来分别处理 get 和 post 请求。具体的代码示例如下所示:

public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Hello World.");
    }
}

除此之外,我们还需要在配置文件 web.xml 中做如下配置。Tomcat、Jetty 等 Servlet 容器在启动的时候,会自动加载这个配置文件中的 URL 和 Servlet 之间的映射关系。

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.xzg.cd.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

当我们在浏览器中输入网址(比如,http://127.0.0.1:8080/hello )的时候,Servlet 容器会接收到相应的请求,并且根据 URL 和 Servlet 之间的映射关系,找到相应的 Servlet(HelloServlet),然后执行它的 service() 方法。service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法,然后输出数据(“Hello world”)到网页。

我们现在来看,HttpServlet 的 service() 函数长什么样子。

public void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;
    if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) {
      throw new ServletException("non-HTTP request or response");
    }
    request = (HttpServletRequest) req;
    response = (HttpServletResponse) res;
    service(request, response);
  }

  protected void service(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
      long lastModified = getLastModified(req);
      if (lastModified == -1) {
        // servlet doesn't support if-modified-since, no reason
        // to go through further expensive logic
        doGet(req, resp);
      } else {
        long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
        if (ifModifiedSince < lastModified) {
          // If the servlet mod time is later, call doGet()
          // Round down to the nearest second for a proper compare
          // A ifModifiedSince of -1 will always be less
          maybeSetLastModified(resp, lastModified);
          doGet(req, resp);
        } else {
          resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
        }
      }
    } else if (method.equals(METHOD_HEAD)) {
      long lastModified = getLastModified(req);
      maybeSetLastModified(resp, lastModified);
      doHead(req, resp);
    } else if (method.equals(METHOD_POST)) {
      doPost(req, resp);
    } else if (method.equals(METHOD_PUT)) {
      doPut(req, resp);
    } else if (method.equals(METHOD_DELETE)) {
      doDelete(req, resp);
    } else if (method.equals(METHOD_OPTIONS)) {
      doOptions(req, resp);
    } else if (method.equals(METHOD_TRACE)) {
      doTrace(req, resp);
    } else {
      String errMsg = lStrings.getString("http.method_not_implemented");
      Object[] errArgs = new Object[1];
      errArgs[0] = method;
      errMsg = MessageFormat.format(errMsg, errArgs);
      resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
  }

从上面的代码中我们可以看出,HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet()、doPost() 是模板中可以由子类来定制的部分。实际上,这就相当于 Servlet 框架提供了一个扩展点(doGet()、doPost() 方法),让框架用户在不用修改 Servlet 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。

JUnit TestCase

  1. 跟 Java Servlet 类似,JUnit 框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown() 等),让框架用户可以在这些扩展点上扩展功能。
  2. 在使用 JUnit 测试框架来编写单元测试的时候,我们编写的测试类都要继承框架提供的 TestCase 类。在 TestCase 类中,runBare() 函数是模板方法,它定义了执行测试用例的整体流程:先执行 setUp() 做些准备工作,然后执行 runTest() 运行真正的测试代码,最后执行 tearDown() 做扫尾工作。
  3. TestCase 类的具体代码如下所示。尽管 setUp()、tearDown() 并不是抽象函数,还提供了默认的实现,不强制子类去重新实现,但这部分也是可以在子类中定制的,所以也符合模板模式的定义。
public abstract class TestCase extends Assert implements Test {
  
  public void runBare() throws Throwable {
    Throwable exception = null;
    setUp();
    try {
      runTest();
    } catch (Throwable running) {
      exception = running;
    } finally {
      try {
        tearDown();
      } catch (Throwable tearingDown) {
        if (exception == null) exception = tearingDown;
      }
    }
    if (exception != null) throw exception;
  }

  /**
   * * Sets up the fixture, for example, open a network connection.
   *
   * <p>* This method is called before a test is executed.
   */
  protected void setUp() throws Exception {}

  /**
   * * Tears down the fixture, for example, close a network connection.
   *
   * <p>* This method is called after a test is executed.
   */
  protected void tearDown() throws Exception {}
}

回调的原理解析

复用和扩展是模板模式的两大作用,实际上,还有另外一个技术概念,也能起到跟模板模式相同的作用,那就是回调(Callback)。今天我们今天就来看一下,回调的原理、实现和应用,以及它跟模板模式的区别和联系。

  1. 相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。
  2. A 类如何将回调函数传递给 B 类呢?不同的编程语言,有不同的实现方法。C 语言可以使用函数指针,Java 则需要使用包裹了回调函数的类对象,我们简称为回调对象。这里我用 Java 语言举例说明一下。代码如下所示:
public interface ICallback {
  void methodToCallback();
}

public class BClass {

  public void process(ICallback callback) {
    // ...
    callback.methodToCallback();
    // ...
  }
}

public class AClass {

  public static void main(String[] args) {
    BClass b = new BClass();
    b.process(
        new ICallback() { // 回调对象
          @Override
          public void methodToCallback() {
            System.out.println("Call back me.");
          }
        });
  }
}
  1. 上面就是 Java 语言中回调的典型代码实现。从代码实现中,我们可以看出,回调跟模板模式一样,也具有复用和扩展的功能。除了回调函数之外,BClass 类的 process() 函数中的逻辑都可以复用。如果 ICallback、BClass 类是框架代码,AClass 是使用框架的客户端代码,我们可以通过 ICallback 定制 process() 函数,也就是说,框架因此具有了扩展的能力。
  2. 实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。
  3. 回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。上面的代码实际上是同步回调的实现方式,在 process() 函数返回之前,执行完回调函数 methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

应用举例一:JdbcTemplate]

  1. Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。而同步回调从应用场景上很像模板模式,所以,在命名上,这些类使用 Template(模板)这个单词作为后缀。
  2. 这些 Template 类的设计思路都很相近,所以,我们只拿其中的 JdbcTemplate 来举例分析一下。对于其他 Template 类,你可以阅读源码自行分析。
  3. 在前面的章节中,我们也多次提到,Java 提供了 JDBC 类库来封装不同类型的数据库操作。不过,直接使用 JDBC 来编写操作数据库的代码,还是有点复杂的。比如,下面这段是使用 JDBC 来查询用户信息的代码。
public class JdbcDemo {
  public User queryUser(long id) {
    Connection conn = null;
    Statement stmt = null;
    try {
      // 1.加载驱动
      Class.forName("com.mysql.jdbc.Driver");
      conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "xzg", "xzg");
      // 2.创建statement类对象,用来执行SQL语句
      stmt = conn.createStatement();
      // 3.ResultSet类,用来存放获取的结果集
      String sql = "select * from user where id=" + id;
      ResultSet resultSet = stmt.executeQuery(sql);
      String eid = null, ename = null, price = null;
      while (resultSet.next()) {
        User user = new User();
        user.setId(resultSet.getLong("id"));
        user.setName(resultSet.getString("name"));
        user.setTelephone(resultSet.getString("telephone"));
        return user;
      }
    } catch (ClassNotFoundException e) {
      // TODO: log...
    } catch (SQLException e) {
      // TODO: log...
    } finally {
      if (conn != null)
        try {
          conn.close();
        } catch (SQLException e) {
          // TODO: log...
        }
      if (stmt != null)
        try {
          stmt.close();
        } catch (SQLException e) {
          // TODO: log...
        }
    }
    return null;
  }
}
  1. queryUser() 函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement、处理异常。针对不同的 SQL 执行请求,这些流程性质的代码是相同的、可以复用的,我们不需要每次都重新敲一遍。

  2. 针对这个问题,Spring 提供了 JdbcTemplate,对 JDBC 进一步封装,来简化数据库编程。使用 JdbcTemplate 查询用户信息,我们只需要编写跟这个业务有关的代码,其中包括,查询用户的 SQL 语句、查询结果与 User 对象之间的映射关系。其他流程性质的代码都封装在了 JdbcTemplate 类中,不需要我们每次都重新编写。我用 JdbcTemplate 重写了上面的例子,代码简单了很多,如下所示:

public class JdbcTemplateDemo {
  private JdbcTemplate jdbcTemplate;

  public User queryUser(long id) {
    String sql = "select * from user where id=" + id;
    return jdbcTemplate.query(sql, new UserRowMapper()).get(0);
  }

  class UserRowMapper implements RowMapper<User> {
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
      User user = new User();
      user.setId(rs.getLong("id"));
      user.setName(rs.getString("name"));
      user.setTelephone(rs.getString("telephone"));
      return user;
    }
  }
}
  1. 那 JdbcTemplate 底层具体是如何实现的呢?我们来看一下它的源码。因为 JdbcTemplate 代码比较多,我只摘抄了部分相关代码,贴到了下面。其中,JdbcTemplate 通过回调的机制,将不变的执行流程抽离出来,放到模板方法 execute() 中,将可变的部分设计成回调 StatementCallback,由用户来定制。query() 函数是对 execute() 函数的二次封装,让接口用起来更加方便。
@Override
  public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
  }

  @Override
  public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
    Assert.notNull(sql, "SQL must not be null");
    Assert.notNull(rse, "ResultSetExtractor must not be null");

    if (logger.isDebugEnabled()) {
      logger.debug("Executing SQL query [" + sql + "]");
    }

    class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
      @Override
      public T doInStatement(Statement stmt) throws SQLException {
        ResultSet rs = null;

        try {
          rs = stmt.executeQuery(sql);
          ResultSet rsToUse = rs;
          if (nativeJdbcExtractor != null) {
            rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
          }
          return rse.extractData(rsToUse);
        } finally {
          JdbcUtils.closeResultSet(rs);
        }
      }

      @Override
      public String getSql() {
        return sql;
      }
    }
    return execute(new QueryStatementCallback());
  }

  @Override
  public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    Connection con = DataSourceUtils.getConnection(getDataSource());
    Statement stmt = null;

    try {
      Connection conToUse = con;
      if (this.nativeJdbcExtractor != null
          && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
        conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
      }
      stmt = conToUse.createStatement();
      applyStatementSettings(stmt);
      Statement stmtToUse = stmt;
      if (this.nativeJdbcExtractor != null) {
        stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
      }
      T result = action.doInStatement(stmtToUse);
      handleWarnings(stmt);
      return result;
    } catch (SQLException ex) {
      // Release Connection early, to avoid potential connection pool deadlock
      // in the case when the exception translator hasn't been initialized yet.
      JdbcUtils.closeStatement(stmt);
      stmt = null;
      DataSourceUtils.releaseConnection(con, getDataSource());
      con = null;
      throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
    } finally {
      JdbcUtils.closeStatement(stmt);
      DataSourceUtils.releaseConnection(con, getDataSource());
    }
  }

应用举例二:setClickListener()

在客户端开发中,我们经常给控件注册事件监听器,比如下面这段代码,就是在 Android 应用开发中,给 Button 控件的点击事件注册监听器。

Button button = (Button)findViewById(R.id.button);
   button.setOnClickListener(new OnClickListener() {
   @Override
   public  void  onClick(View v)  {
     System.out.println("I am clicked.");
   }
 });
  1. 从代码结构上来看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又很像观察者模式,即事先注册观察者(OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的 onClick() 函数。
  2. 我们前面讲到,回调分为同步回调和异步回调。这里的回调算是异步回调,我们往 setOnClickListener() 函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式。

应用举例三:addShutdownHook()

  1. Hook 可以翻译成“钩子”,那它跟 Callback 有什么区别呢?
  2. 网上有人认为 Hook 就是 Callback,两者说的是一回事儿,只是表达不同而已。而有人觉得 Hook 是 Callback 的一种应用。Callback 更侧重语法机制的描述,Hook 更加侧重应用场景的描述。我个人比较认可后面一种说法。不过,这个也不重要,我们只需要见了代码能认识,遇到场景会用就可以了。
  3. Hook 比较经典的应用场景是 Tomcat 和 JVM 的 shutdown hook。接下来,我们拿 JVM 来举例说明一下。JVM 提供了 Runtime.addShutdownHook(Thread hook) 方法,可以注册一个 JVM 关闭的 Hook。当应用程序关闭的时候,JVM 会自动调用 Hook 代码。代码示例如下所示:
public class ShutdownHookDemo {
  private static class ShutdownHook extends Thread {
    public void run() {
      System.out.println("I am called during shutting down.");
    }
  }

  public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new ShutdownHook());
  }
}

我们再来看 addShutdownHook() 的代码实现,如下所示。这里我只给出了部分相关代码。

public class Runtime {
  public void addShutdownHook(Thread hook) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(new RuntimePermission("shutdownHooks"));
    }
    ApplicationShutdownHooks.add(hook);
  }
}

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;
    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }


    private ApplicationShutdownHooks() {}

    /* Add a new shutdown hook.  Checks the shutdown state and the hook itself,
     * but does not do any security checks.
     */
    static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

    /* Remove a previously-registered hook.  Like the add method, this method
     * does not do any security checks.
     */
    static synchronized boolean remove(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook == null)
            throw new NullPointerException();

        return hooks.remove(hook) != null;
    }

    /* Iterates over all application hooks creating a new thread for each
     * to run in. Hooks are run concurrently and this method waits for
     * them to finish.
     */
    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
}

从代码中我们可以发现,有关 Hook 的逻辑都被封装到 ApplicationShutdownHooks 类中了。当应用程序关闭的时候,JVM 会调用这个类的 runHooks() 方法,创建多个线程,并发地执行多个 Hook。我们在注册完 Hook 之后,并不需要等待 Hook 执行完成,所以,这也算是一种异步回调。

模板模式 VS 回调

  1. 回调的原理、实现和应用到此就都讲完了。接下来,我们从应用场景和代码实现两个角度,来对比一下模板模式和回调。
  2. 从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
  3. 从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
  4. 前面我们也讲到,组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点。
  • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

文章作者: Prannt
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Prannt !
评论
  目录