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

简介: JAVA设计模式第五讲:设计模式在 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

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
3天前
|
设计模式 Java API
Java 可扩展 API 设计:打造灵活的应用架构
【4月更文挑战第27天】设计可扩展的 API 是构建灵活、易于维护的应用程序架构的关键。Java 提供了丰富的工具和技术来实现这一目标,使开发者能够构建具有高度可扩展性的应用程序。
20 4
|
3天前
|
Java
【专栏】Java中的反射机制与应用实例
【4月更文挑战第27天】本文探讨了Java反射机制,该机制允许程序在运行时获取类信息、动态创建对象、调用方法和访问属性。反射通过Class、Constructor、Method和Field类实现。文中列举了反射的应用场景,如动态创建对象、调用方法、访问属性和处理注解,并提供了相关实例代码演示。
|
1天前
|
Java
Java中的条件语句结构在编程中的应用
Java中的条件语句结构在编程中的应用
5 0
|
1天前
|
敏捷开发 机器学习/深度学习 Java
Java中的异常处理机制深入理解与实践:持续集成在软件测试中的应用探索自动化测试在敏捷开发中的关键作用
【4月更文挑战第29天】在Java编程中,异常处理是一个重要的概念。它允许开发者在程序执行过程中遇到错误或异常情况时,能够捕获并处理这些异常,从而保证程序的稳定运行。本文将详细介绍Java中的异常处理机制,包括异常的分类、异常的处理方式以及自定义异常等内容。 【4月更文挑战第29天】 随着敏捷开发和DevOps文化的兴起,持续集成(CI)已成为现代软件开发周期中不可或缺的一环。本文将探讨持续集成在软件测试领域内的关键作用、实施策略以及面临的挑战。通过对自动化构建、测试用例管理、及时反馈等核心要素的详细分析,揭示持续集成如何提高软件质量和加速交付过程。 【4月更文挑战第29天】 在当今快速发
|
2天前
|
弹性计算 运维 Java
Serverless 应用引擎产品使用之在Serverless 应用引擎中,将 Java 应用从 ECS 迁移到 SAE如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
22 2
|
2天前
|
监控 搜索推荐 算法
Java排序:原理、实现与应用
【4月更文挑战第28天】本文探讨了Java中的排序算法,包括原理和实现。Java利用Comparator接口进行元素比较,通过Arrays和Collections类的sort方法对数组和列表进行排序。示例展示了使用这些方法的基本代码。此外,还讨论了冒泡排序算法和自定义排序场景,以适应不同需求。理解这些排序机制有助于提升程序效率。
8 1
|
3天前
|
监控 Java API
Java 模块化设计:概念与实战应用
【4月更文挑战第27天】模块化设计是现代软件开发的关键,它帮助开发者构建可管理、可维护的大型系统。Java 平台的模块化支持始于 Java 9,引入了一种全新的模块系统。
12 3
|
3天前
|
设计模式 消息中间件 Java
Java 设计模式:探索发布-订阅模式的原理与应用
【4月更文挑战第27天】发布-订阅模式是一种消息传递范式,被广泛用于构建松散耦合的系统。在 Java 中,这种模式允许多个对象监听和响应感兴趣的事件。
20 2
|
3天前
|
设计模式 算法 Java
Java 设计模式:深入模板方法模式的原理与应用
【4月更文挑战第27天】模板方法模式是一种行为设计模式,主要用于定义一个操作中的算法的框架,允许子类在不改变算法结构的情况下重定义算法的某些特定步骤。
12 1
|
3天前
|
设计模式 算法 Java
Java 设计模式:探索策略模式的概念和实战应用
【4月更文挑战第27天】策略模式是一种行为设计模式,它允许在运行时选择算法的行为。在 Java 中,策略模式通过定义一系列的算法,并将每一个算法封装起来,并使它们可以互换,这样算法的变化不会影响到使用算法的客户。
9 1