JAVA设计模式第五讲:设计模式在 Google Guava 的应用

摘要设计模式(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层。
      秒杀系统
  • 服务解耦
    • 引入消息队列后,上游服务在业务变化时发送一条消息到消息队列的一个主题中,所有下游系统都订阅该主题。无论增加、减少下游系统或是下游系统需求如何变化,上游系统都无需做任何更改,实现了上游服务与下游服务的解耦。

参考文献

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员 jet_qi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值