摘要:设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案,本文先介绍设计模式概念及应用场景,然后讲解设计模式在 Google Guava 中的应用,分析其原理;最后分析在使用过程中可以优化的地方,本文是设计模式第五讲:设计模式在 Google Guava 的应用
1、设计模式概念及使用场景
设计模式是什么:设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚各种模式能解决什么问题。
总体来说设计模式可以分为三大类:
使用场景:在项目开发中解耦代码,让我们写出易拓展的代码
- 创建型模式是将创建和使用代码解耦,
- 结构型模式是将不同功能代码解耦,
- 行为型模式是将不同的行为代码解耦。
1.1、创建型模式
- 共5种:是对对象创建过程中各种问题和解决方案的总结
- 单例模式
- 创建全局唯一的对象
- 工厂方法模式(抽象工厂模式)
- 工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象
- Builder 模式
- Builder 模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象
- 原型模式
- 利用对已有对象进行复制的方式,来创建新对象
1.2、结构型模式
- 共7种:关注于类/对象的继承/组合方式
- 代理模式
- 可以在目标对象实现的基础上,增强额外的功能操作,扩展目标对象的功能
- 桥接模式
- 将实现与抽象放在两个不同的类层次中,使这两个层次可以独立改变
- 适配器模式
- 消除接口不匹配所造成的类的兼容性问题
- 装饰器模式
- 通过组合来替代继承,动态地将新功能附加到对象上
- 外观模式
- 定义一组高层接口让子系统更易用
- 组合模式
- 将对象组合成树状结构以表示 “整体-部分”的层次关系
- 享元模式
- 运用共享技术支持大量细粒度的对象
1.3、行为型模式
- 共11种:是从类或对象之间交互/职责划分等角度总结的模式
- 观察者模式
- 当一个对象状态改变的时候,所有依赖的对象都会自动收到通知
- 模板方法模式
- 定义业务逻辑骨架,并将某些步骤推迟到子类中实现
- 策略模式
- 定义一组策略类,将每个策略分别封装起来,让它们可以互相替换
- 迭代器模式
- 将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,专用来遍历集合对象
- 职责链模式
- 使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系
- 状态模式
- 是状态机的一种实现方式,封装转换规则,并枚举可能的状态
- 命令模式
- 将不同请求封装为对象,以控制命令的执行
- 备忘录模式
- 在不违背封装原则的前提下,进行对象的备份和恢复
- 访问者模式
- 允许一个或者多个操作应用到一组对象上,解耦操作和对象本身
- 中介者模式
- 引入中间层,将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互
- 解释器模式
- 根据语法规则,定义一个解释器用来处理这个语法
2、设计模式在 Guava 中的使用
2.1 Google Guava 是什么?
Google 公司内部 Java 开发工具库的开源版本。它提供了一些 JDK 没有提供的功能,以及对 JDK 已有功能的增强功能。包括:集合(Collections)、缓存(Caching)、原生类型支持(Primitives Support)、并发库(Concurrency Libraries)、通用注解(Common Annotation)、字符串处理(Strings Processing)、数学计算(Math)、I/O、事件总线(EventBus)等等。
Google Guava 中用到的几种经典设计模式:观察者模式、Builder 模式、装饰器模式, Immutable 模式。
2.2、观察者模式在 Guava 中的使用 --EventBus
观察者模式定义:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer),也可以被称为发布-订阅模式。
观察者模式应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都涉及这种模式,例如,邮件订阅、EventBus、MQ,本质上都是观察者模式。
**EventBus 实现了异步非阻塞观察者模式 **: Java 的进程内事件分发都是通过发布者和订阅者之间显示注册实现的,设计 EventBus 就是为了取代这种显示注册的方式,使组件间能更好地解耦合。
案例:用户在注册后需要通知给业务方A处理,也需要发送给业务方B处理,EventBus 代码演示如下所示:
// 被观察者
public class UserController {
@Autowired
private UserService userService;
private EventBus eventBus;
private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
public UserController() {
// 异步非阻塞模式,可以在构造器中使用独立线程池
eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_SIZE));
}
public void setRegObservers(List<Object> observers) {
for (Object observer : observers) {
// 可以接收任何类型观察者
eventBus.register(observer);
}
}
public Long register(String telephone, String password) {
// 用户注册
long userId = userService.register(telephone, password);
// 用于给观察者发消息,规则:能接受的消息类型是发送消息类型的父类或相同类型
MessageEvent event = new MessageEvent();
event.setUserId(userId);
eventBus.post(event);
return userId;
}
}
// 观察者1,任意类型的对象都可以注册到 EventBus 中
public class AObserver {
// 依赖注入
private AService aService;
// EventBus 会根据该注解找到方法,将方法能接收到的消息类型记录下来
@Subscribe
public void process(MessageEvent event) {
// todo
}
}
// 观察者2
public class RegNotificationObserver {
private BService bService;
@Subscribe
public void process(MessageEvent event) {
// todo
}
}
EventBus作为一个总线,还考虑了递归传送事件的问题,可以选择广度优先传播和深度优先传播,遇到事件死循环的时候还会报错。Guava 项目对这个模块的封装非常值得我们去阅读,复杂的逻辑都封装在里头,提供的对外接口极为易用。
下面讲解使用 EventBus 的注意事项并分析其原理
(1)使用 EventBus 注意事项:
① EventBus 实现了同步阻塞的观察者模式,AsyncEventBus 继承自 EventBus,提供了异步非阻塞的观察者模式。在 AsyncEventBus 中,可以在构造器中使用独立线程池,防止某个事件很忙导致其余事件产生饥饿的情况;
② 观察者通过 @Subscribe 注解定义能接收的消息类型,调用 post() 函数发送消息的时候,能接收观察者消息类型是发送消息(post 函数定义中的 event)类型的父类;
③ EventBus 没有采用单例模式,如果想在进程范围内使用唯一的事件总线,可以将其声明为全局单例。
(2)原理分析:
源码中最关键的数据结构是 Observer 注册表,它记录了消息类型和可接收消息函数的对应关系。当调用 register() 函数注册观察者时,EventBus 通过解析 @Subscribe 注解,生成 Observer 注册表。当调用 post() 函数发送消息的时候,EventBus 通过注册表找到相应的可接收消息的函数,然后通过 Java 反射语法来动态地创建对象、执行函数。对于异步非阻塞模式,EventBus 通过一个线程池来执行相应的函数。
2.3、Builder 模式在 Guava 中的应用
Builder 模式定义:Builder 模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。将复杂对象的建造过程抽象出来。
Builder 模式应用场景:通常的做法是在构建对象时,必填项使用有参构造函数,非必填属性使用 set() 方法,如果符合下面的条件:① 当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数;② 类的属性之间有一定的依赖关系或者约束条件;③希望创建不可变对象,也就是说,不能在类中暴露 set() 方法。那么应该使用 Builder 设计模式来创建对象。
如上一篇文章所述:Guava Cache 实战–从场景使用到原理分析
我们经常用到缓存来提高访问速度。构建内存缓存的方式为:① 基于 JDK 提供的类,比如 HashMap,从零开始开发内存缓存。缺点是涉及的工作比较多,比如缓存淘汰策略等。② 缓存系统有 Redis、Memcache 等,如果要缓存的数据比较少,没必要在项目中独立部署一套缓存系统。③ 为了简化开发,我们可以使用 Google Guava 提供的现成的缓存工具类com.google.common.cache.*
Guava cache 代码演示如下所示:
public class CacheDemo {
public static void main(String[] args) {
Cache<String, String> cache = CacheBuilder.newBuilder()
//设置 cache 的初始大小为100
.initialCapacity(100)
//最大 key 个数
.maximumSize(1000)
//设置 cache 中的数据在写入之后的存活时间为10秒
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
cache.put("key1", "value1");
String value = cache.getIfPresent("key1");
System.out.println(value);
}
}
Cache 对象是通过 CacheBuilder 类来创建的,为什么要使用这种方式呢?
构建缓存,需要配置大量参数,比如过期时间、淘汰策略、最大缓存大小等等。相应地,Cache 类就会包含很多成员变量。我们需要在构造函数中,设置这些成员变量的值,但又不是所有的值都必须设置,设置哪些值由用户来决定。为了满足这个需求,我们就需要定义多个包含不同参数列表的构造函数。为了避免构造函数的参数列表过长、不同的构造函数过多,我们一般有两种解决方案。
方案1:使用 Builder 模式;
方案2:先通过无参或有参构造函数创建对象,然后再通过 setXXX() 方法来逐一设置成员变量。
为什么不使用方案2 ?
我们可以查看 CacheBuilder 类中的 build() 方法源码,如下所示
public final class CacheBuilder<K, V> {
...
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
checkWeightWithWeigher();
checkNonLoadingCache();
return new LocalCache.LocalManualCache<K1, V1>(this);
}
private void checkNonLoadingCache() {
checkState(refreshNanos == UNSET_INT, "refreshAfterWrite requires a LoadingCache");
}
private void checkWeightWithWeigher() {
if (weigher == null) {
checkState(maximumWeight == UNSET_INT, "maximumWeight requires weigher");
} else {
if (strictParsing) {
checkState(maximumWeight != UNSET_INT, "weigher requires maximumWeight");
} else {
if (maximumWeight == UNSET_INT) {
logger.log(Level.WARNING, "ignoring weigher specified without maximumWeight");
}
}
}
}
}
由上可知,在构造 Cache 对象时,类的属性之间有一定的依赖关系或者约束条件,为了创建 Cache 对象的合法性,因此 Guava Cache 使用了 Builder 设计模式。
2.4、装饰器模式在 Guava 中的应用
装饰器模式定义:主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能,能动态地将新功能附加到对象上。
应用场景:当我们需要修改原有功能,但又不愿直接去修改原有代码时,设计一个Decorator 套在原有代码外面。
装饰者模式的特点总结:①装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类;②装饰器类的**成员变量类型为父类类型;**③装饰器类是对功能的增强
在 Google Guava 的 collection 包路径下,有一组以 Forwarding 开头命名的类,代码如下所示
@GwtCompatible
public abstract class ForwardingCollection<E> extends ForwardingObject implements Collection<E> {
protected ForwardingCollection() {}
@Override
protected abstract Collection<E> delegate();
@Override
public Iterator<E> iterator() {
return delegate().iterator();
}
@Override
public int size() {
return delegate().size();
}
@Override
public boolean isEmpty() {
return delegate().isEmpty();
}
@Override
public boolean contains(Object object) {
return delegate().contains(object);
}
@Override
public boolean add(E element) {
return delegate().add(element);
}
@Override
public boolean remove(Object object) {
return delegate().remove(object);
}
@Override
public boolean containsAll(Collection<?> collection) {
return delegate().containsAll(collection);
}
@Override
public boolean addAll(Collection<? extends E> collection) {
return delegate().addAll(collection);
}
@Override
public boolean retainAll(Collection<?> collection) {
return delegate().retainAll(collection);
}
...
}
装饰器类代码如下所示,ConstrainedList 是基于 装饰器模式实现的一个类,在原始 Collection 类的基础上,针对自身业务场景,做了方法增强操作。好处是:装饰者类可以只实现自己关注的方法,其他方法使用缺省 Forwarding 类的实现,简化了代码量。
// ConstrainedList<E> ForwardingList<E> 均为装饰器类,其中 ForwardingList 继承自 ForwardingCollection<E>
@GwtCompatible
private static class ConstrainedList<E> extends ForwardingList<E> {
// 原始类,成员变量为父类类型
final List<E> delegate;
final Constraint<? super E> constraint;
ConstrainedList(List<E> delegate, Constraint<? super E> constraint) {
this.delegate = checkNotNull(delegate);
this.constraint = checkNotNull(constraint);
}
@Override
protected List<E> delegate() {
return delegate;
}
@Override
public boolean add(E element) {
// 在执行委托方法前,对入参做了校验
constraint.checkElement(element);
return delegate.add(element);
}
@Override
public void add(int index, E element) {
constraint.checkElement(element);
delegate.add(index, element);
}
@Override
public boolean addAll(Collection<? extends E> elements) {
return delegate.addAll(checkElements(elements, constraint));
}
@Override
public boolean addAll(int index, Collection<? extends E> elements) {
return delegate.addAll(index, checkElements(elements, constraint));
}
@Override
public E set(int index, E element) {
constraint.checkElement(element);
return delegate.set(index, element);
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return constrainedList(delegate.subList(fromIndex, toIndex), constraint);
}
}
**2.5、Immutable 模式在 Guava 中的应用 **
Immutable 模式定义:一个对象的状态在对象创建之后就不再改变。其中涉及的类就是不变类(Immutable Class),对象就是不变对象(Immutable Object)。注意:它并不属于经典的23中设计模式
应用场景:如果一个对象符合创建之后就不会被修改,即可使用 Immutable 模式,常用在多线程环境下。
Immutable 示例代码如下所示,对象中包含的引用对象是可以改变的,这点类似于原型设计模式中的浅拷贝
// Immutable 模式, 可以使用 Builder 模式来替换,同样达到不可变类的效果
public class User {
private String name;
private int age;
// 引用类型
private Address addr;
public User(String name, int age, Address addr) {
this.name = name;
this.age = age;
this.addr = addr;
}
// 只有getter方法,无setter方法...
}
public class Address {
private String province;
private String city;
public Address(String province, String city) {
this.province = province;
this.city= city;
}
// 有getter方法,也有setter方法...
}
Google Guava 针对集合类(Collection、List、Set、Map…)提供了对应的不变集合类(ImmutableCollection、
ImmutableList、ImmutableSet、ImmutableMap…),在Java JDK 中也提供了不变集合类(UnmodifiableCollection、UnmodifiableList、UnmodifiableSet、UnmodifiableMap…)。
那两者不变集合类的区别在哪里呢? 示例代码如下所示:
public class ImmutableDemo {
public static void main(String[] args) {
List<String> originalList = new ArrayList<>();
originalList.add("a");
originalList.add("b");
originalList.add("c");
List<String> jdkUnmodifiableList = Collections.unmodifiableList(originalList);
List<String> guavaImmutableList = ImmutableList.copyOf(originalList);
// jdkUnmodifiableList.add("d"); // 抛出UnsupportedOperationException
// guavaImmutableList.add("d"); // 抛出UnsupportedOperationException
originalList.add("d");
// a,b,c,d
print(originalList);
// a,b,c,d
print(jdkUnmodifiableList);
// a,b,c
print(guavaImmutableList);
}
private static void print(List<String> list) {
String join = Joiner.on(",").skipNulls().join(list);
System.out.println(join);
}
}
可以看出,当原始集合增加数据之后,JDK 不变集合的数据随之增加,而 GoogleGuava 的不变集合的数据并没有增加。
原因如下:JDK 的不变集合相当于对原集合采用装饰者模式,即通过组合方式限制掉原集合的写操作,所以在原始集合类发生改变的时候它也会改变,而 Google Guava 的不变集合,是重新创建了一个原始集合对象的副本,所以改变原始类并不能改变它的数据,也更加符合语义。在日常使用时,需要注意这一点。
3、可以优化的地方
(1)EventBus 缺点及可以优化的地方:
①EventBus 没有持久化机制,没有重试机制;
②EventBus 的异步处理,是直接丢在同一个线程池处理,存在某个事件很忙导致其余事件饥饿的情况,因此给每个任务都需要自定义线程池;
③ event 需要加 @AllowConcurrentEvents 标识其线程安全时,否则在执行方法的过程是加了 synchronized 关键字控制的,锁的粒度太大;
④当事件监听者过多或者项目中监听者过多时,由于没有平台能查看其依赖关系,因此程序调试困难
由于 EventBus 的上述缺点,它的使用场景局限在耗时且不重要的业务 例如记录日志,消息通知,数据统计等
如果是需要保持数据一致性,需要接入 MQ 来保证业务的准确性。
下面简要介绍下 MQ 的使用场景?
- 流量控制
- MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮。
- 异步处理
- 上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。
秒杀系统
- 上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。
- 服务解耦
- 引入消息队列后,上游服务在业务变化时发送一条消息到消息队列的一个主题中,所有下游系统都订阅该主题。无论增加、减少下游系统或是下游系统需求如何变化,上游系统都无需做任何更改,实现了上游服务与下游服务的解耦。
参考文献
1、http://ifeve.com/google-guava-eventbus/
2、https://github.com/greenrobot/EventBus
3、https://time.geekbang.org/column/article/234758
4、https://juejin.cn/post/7030979943068598303