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版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
18天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
27天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
20天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
33 3
|
1月前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
32 7
|
28天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
43 3
|
5天前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践感悟####
本文作为一篇技术性文章,旨在深入探讨Python编程中设计模式的应用价值与实践心得。在快速迭代的软件开发领域,设计模式如同导航灯塔,指引开发者构建高效、可维护的软件架构。本文将通过具体案例,展现设计模式如何在实际项目中解决复杂问题,提升代码质量,并分享个人在实践过程中的体会与感悟。 ####
|
28天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
51 2
|
29天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
13 1
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
19 1
|
2月前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。