设计模式学习笔记 - 开源实战三(中):剖析Google Guava中用到的设计模式

2024-04-21 07:28

本文主要是介绍设计模式学习笔记 - 开源实战三(中):剖析Google Guava中用到的设计模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概述

上篇文章,我通过 Google Guava 这样一个优秀的开源类库,讲解了如何在业务开发中,发现跟业务无关、可以复用的通用功能模块,并将它们抽离出来,设计成独立的类库、框架或功能组件。

本章再来学习下,Google Guava 中用到的几中经典的设计模式:Builder 模式、Wrapper 模式,以及之前没有讲过的 Immutable 模式。


Builder 模式在 Google Guava 中的应用

在项目开发中,我们常用到缓存,它可以有效地提高访问速度。

常用的缓存系统有 Redis、Memcache 等。但是,如果要缓存的数据比较少,我们完全没必要再项目中独立部署一套缓存系统。毕竟系统都有一定的出错率,项目中包含的系统越多,那组合起来,项目整体出错的几率就会升高,可用性就会降低。同时,多引入一个系统就要多维护一个系统,项目的维护成本就会变高。

取而代之,我们可以在系统内部构件一个内存缓存,跟系统集成在一起开发、部署。那如何构建内存缓存呢? 我们可以基于 JDK 提供的类。比如 HashMap ,从零开始开发内存缓存。不过,从零开发一个缓存,涉及的工作会比较多,比如缓存淘汰策略等。为了简化开发,我们就可以使用 Google Guava 提供的线程的缓存工具类 com.google.connom.cache.*

使用 Google Guava 来构建内存缓存非常简单,下面是我我写的一个例子。

public class CacheDemo {public static void main(String[] args) {Cache<String, String> cache = CacheBuilder.newBuilder().initialCapacity(100).maximumSize(1000).expireAfterWrite(10, TimeUnit.SECONDS).build();cache.put("key1", "value1");String value = cache.getIfPresent("key1");System.out.println(value);}
}

从上面的代码可以看出,Cache 对象是通过 CacheBuilder 这样一个 Builder 类来创建的。为什么要由 Builder 类来创建 Cache 对象呢?这个问题现在对你来说应该没有难度了吧,在建造者模式章节,已进行了详细的讲解了。

构建一个缓存,需要配置 n 多个参数,比如过期时间、淘汰策略、最大缓存大小等等。相应地,Cache 类就会包含 n 多成员变量。我们需要在构造函数中,设置这些成员变量的值,但又不是所有的值都必须设置,设置哪些由用户来决定。为了满足这个需求,我们就需要定义多个包含不同参数列表的构造函数。

为了避免构造函数的参数列表过长、不同的构造函数过多,一般由两种解决方案。其中,一个解决方案是使用 Builder 模式。另一个方案是先通过无参构造函数创建对象,然后再通过 setXXX() 方法来逐一设置成员变量。

为什么 Google Guava 选择第一种而不是第二种解决方案呢?使用第二种解决方案是否也可以呢?大难是不行的。至于为什么,看下面的源码就清楚了。我们把 CacheBuilder 类中的 build() 函数摘抄到了下面。

    public <K1 extends K, V1 extends V> Cache<K1, V1> build() {this.checkWeightWithWeigher();this.checkNonLoadingCache();return new LocalManualCache(this);}private void checkNonLoadingCache() {Preconditions.checkState(this.refreshNanos == -1L, "refreshAfterWrite requires a LoadingCache");}private void checkWeightWithWeigher() {if (this.weigher == null) {Preconditions.checkState(this.maximumWeight == -1L, "maximumWeight requires weigher");} else if (this.strictParsing) {Preconditions.checkState(this.maximumWeight != -1L, "weigher requires maximumWeight");} else if (this.maximumWeight == -1L) {logger.log(Level.WARNING, "ignoring weigher specified without maximumWeight");}}

必须使用 Builder 模式的主要原因是,在真正构造 Cache 对象时,必须做一些必要的参数校验,也就是 build() 函数中的前两行代码要做的工作。如果采用无参默认构造函数加 setXXX() 方法的方案,这个校验就无处安放了。而不经过校验,创建的 Cache 对象有可能是不合法的,不可用的。

Wrapper 模式在 Guava 中的应用

在 Google Guava 的 collection 包路径下,有一组以 Forwarding 开头命名的类。

在这里插入图片描述
这组 Forwarding 开头命名的类虽然很多,但实现方式都很相似。下面是照抄了其中的 ForwardingCollection 中的部分代码,你可以思考下这组 Forwarding 类是干什么用的。

@GwtCompatible
public abstract class ForwardingCollection<E> extends ForwardingObject implements Collection<E> {// TODO(lowasser): identify places where thread safety is actually lost/** Constructor for use by subclasses. */protected ForwardingCollection() {}@Overrideprotected abstract Collection<E> delegate();@Overridepublic Iterator<E> iterator() {return delegate().iterator();}@Overridepublic int size() {return delegate().size();}@CanIgnoreReturnValue@Overridepublic boolean removeAll(Collection<?> collection) {return delegate().removeAll(collection);}@Overridepublic boolean isEmpty() {return delegate().isEmpty();}@Overridepublic boolean contains(Object object) {return delegate().contains(object);}@CanIgnoreReturnValue@Overridepublic boolean add(E element) {return delegate().add(element);}@CanIgnoreReturnValue@Overridepublic boolean remove(Object object) {return delegate().remove(object);}@Overridepublic boolean containsAll(Collection<?> collection) {return delegate().containsAll(collection);}@CanIgnoreReturnValue@Overridepublic boolean addAll(Collection<? extends E> collection) {return delegate().addAll(collection);}@CanIgnoreReturnValue@Overridepublic boolean retainAll(Collection<?> collection) {return delegate().retainAll(collection);}@Overridepublic void clear() {delegate().clear();}@Overridepublic Object[] toArray() {return delegate().toArray();}// ...
}

光看 ForwardingCollection 的代码实现,你可能想不到它的作用。下面是一个它的用法示例。

public class AddLoggingCollection<E> extends ForwardingCollection<E> {private static final Logger logger = LoggerFactory.getLogger(AddLoggingCollection.class);private Collection<E> originalCollection;public AddLoggingCollection(Collection<E> originalCollection) {this.originalCollection = originalCollection;}@Overrideprotected Collection<E> delegate() {return this.originalCollection;}@Overridepublic boolean add(E element) {logger.info("Add element: " + element);return this.delegate().add(element);}@Overridepublic boolean addAll(Collection<? extends E> collection) {logger.info("Size of elements to add: " + collection.size());return this.delegate().addAll(collection);}
}

在上面的代码中, AddLoggingCollection 是基于代理模式实现的一个代理类,它在原始 Collection 类的基础上,针对 Add 相关操作,添加了记录日志的功能。

前面讲过,代理模式、装饰器、适配器模式都可以成为 Wapper 模式,通过 Wrapper 类二次封装原始类。它们的代码也很相似,都可以通过组合的方式,将 Wrapper 类的函数实现委托给原始类的函数来实现。

public interface Interf {void f1();void f2();
}
public class OriginalClass implements Interf {@Overridepublic void f1() {// ...}@Overridepublic void f2() {// ...}
}
public class WrapperClass implements Interf {private OriginalClass originalClass;public WrapperClass(OriginalClass originalClass) {this.originalClass = originalClass;}@Overridepublic void f1() {// 附加功能...originalClass.f1();// 附加功能...}@Overridepublic void f2() {originalClass.f2();}
}

实际上,这个 ForwardingCollection 类是一个 “默认 Wrapper 类” 或者叫 “缺省 Wrapper 类”。它类似在装饰器模式章节中,讲到的 FilterInputStream。你可以回头去看下。

如果我们不使用这个 ForwardingCollection,而是让 AddLoggingCollection 类直接实现 Collection 接口,那 Collection 接口中的所有方法,都要在 AddLoggingCollection 类中实现一遍,而真正需要添加日志的功能只有 add()addAll() 两个函数,其他函数的实现,都只是类似 Wrapper 类中的 f2() 函数的实现那样,简单地委托给原始 Collection 类对象的对应函数。

为了简化 Wrapper 模式的代码实现,Guava 提供一系列缺省的 Forwarding 类。用户在实现自己的 Wrapper 类时,基于缺省的 Forwarding 类来扩展,就可以只实现自己关心的方法,其他不关心的方法使用缺省 Forwarding 类的实现,就像 AddLoggingCollection 类的实现那样。

Immutable 模式在 Guava 中的应用

Immutable 模式,中文叫不变模式,它不属于经典的 23 种设计模式,但作为一种较常用的设计思路,可以总结为一种设计模式来学习。一个对象的状态在对象创建之后就不再改变,这就是所谓的不变模式。其中涉及的类就是不变类(Immutable Class),对象就是不变对象(Immutable Object)。在 Java 中,最常用的不变类就是 String 类,String 对象一旦创建之后就无法改变。

不可变模式分为两类,一类是普通模式不变模式,另一类是深度不变模式(Deeply Immutable Pattern)。

  • 普通不变模式指的是,对象中包含的引用对象是可变的。如果不特别说明,通常我们所说的不变模式,指的就是普通的不变模式。
  • 深度不变模式指的是,对象包含的引用对象也不可能。

它们之间的关系,有点类似之前讲过的浅拷贝和深拷贝之间的关系。下面是一个示例代码:

// 普通不变模式
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方法...
}// 深度不变模式
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方法...
}

在某个业务场景下,如果一个对象符合创建之后不会被修改这个特性,那我们就可以把它设计成不变类。显示地强制它不可变,这样能避免意外被修改。那如何将一个类设置为不可变类呢?其实方法很简单,只要这个类满足:所有成员变量都通过构造函数一次性设置好,不暴露任何 set 等修改成员变量的方法。此外,因为数据不变,所以不存在并发读写问题,因此不变模式常用在多线程环境下,来避免线程加锁。所以,不变模式也常被归为多线程设计模式。

接下来,我们来看一下特殊的不变类,那就是不变集合。Google Guava 针对集合(CollectionListSetMap…)提供了对应地不变集合类(ImmutableCollectionImmutableListImmutableSetImmutableMap…)。刚刚讲过,不变模式分为两种,普通不变模式和深度不变模式。Google Guava 提供的不变集合类属于前者,也就是说集合中的对象不会增删,但对象的成员变量是可以改变的。

实际上,JDK 也提供了不变集合类(UnmodifiableCollectionUnmodifiableListUnmodifiableSetUnmodifiableMap…)。它和 Google Guava 提供的不便集合类的区别在哪里呢?我举个例子就明白,代码如下所示:

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"); // 抛出UnsupportedOperationExceptionoriginalList.add("d");// 输出结果:// a b c d // a b c d // a b c print(originalList);print(jdkUnmodifiableList);print(guavaImmutableList);}private static void print(List<String> list) {for (String s : list) {System.out.print(s + " ");}System.out.println();}
}

这篇关于设计模式学习笔记 - 开源实战三(中):剖析Google Guava中用到的设计模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/922494

相关文章

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

Java MQTT实战应用

《JavaMQTT实战应用》本文详解MQTT协议,涵盖其发布/订阅机制、低功耗高效特性、三种服务质量等级(QoS0/1/2),以及客户端、代理、主题的核心概念,最后提供Linux部署教程、Sprin... 目录一、MQTT协议二、MQTT优点三、三种服务质量等级四、客户端、代理、主题1. 客户端(Clien

在Spring Boot中集成RabbitMQ的实战记录

《在SpringBoot中集成RabbitMQ的实战记录》本文介绍SpringBoot集成RabbitMQ的步骤,涵盖配置连接、消息发送与接收,并对比两种定义Exchange与队列的方式:手动声明(... 目录前言准备工作1. 安装 RabbitMQ2. 消息发送者(Producer)配置1. 创建 Spr

深度解析Spring Boot拦截器Interceptor与过滤器Filter的区别与实战指南

《深度解析SpringBoot拦截器Interceptor与过滤器Filter的区别与实战指南》本文深度解析SpringBoot中拦截器与过滤器的区别,涵盖执行顺序、依赖关系、异常处理等核心差异,并... 目录Spring Boot拦截器(Interceptor)与过滤器(Filter)深度解析:区别、实现

深度解析Spring AOP @Aspect 原理、实战与最佳实践教程

《深度解析SpringAOP@Aspect原理、实战与最佳实践教程》文章系统讲解了SpringAOP核心概念、实现方式及原理,涵盖横切关注点分离、代理机制(JDK/CGLIB)、切入点类型、性能... 目录1. @ASPect 核心概念1.1 AOP 编程范式1.2 @Aspect 关键特性2. 完整代码实

MySQL中的索引结构和分类实战案例详解

《MySQL中的索引结构和分类实战案例详解》本文详解MySQL索引结构与分类,涵盖B树、B+树、哈希及全文索引,分析其原理与优劣势,并结合实战案例探讨创建、管理及优化技巧,助力提升查询性能,感兴趣的朋... 目录一、索引概述1.1 索引的定义与作用1.2 索引的基本原理二、索引结构详解2.1 B树索引2.2

从入门到精通MySQL 数据库索引(实战案例)

《从入门到精通MySQL数据库索引(实战案例)》索引是数据库的目录,提升查询速度,主要类型包括BTree、Hash、全文、空间索引,需根据场景选择,建议用于高频查询、关联字段、排序等,避免重复率高或... 目录一、索引是什么?能干嘛?核心作用:二、索引的 4 种主要类型(附通俗例子)1. BTree 索引(

Java Web实现类似Excel表格锁定功能实战教程

《JavaWeb实现类似Excel表格锁定功能实战教程》本文将详细介绍通过创建特定div元素并利用CSS布局和JavaScript事件监听来实现类似Excel的锁定行和列效果的方法,感兴趣的朋友跟随... 目录1. 模拟Excel表格锁定功能2. 创建3个div元素实现表格锁定2.1 div元素布局设计2.

Redis 配置文件使用建议redis.conf 从入门到实战

《Redis配置文件使用建议redis.conf从入门到实战》Redis配置方式包括配置文件、命令行参数、运行时CONFIG命令,支持动态修改参数及持久化,常用项涉及端口、绑定、内存策略等,版本8... 目录一、Redis.conf 是什么?二、命令行方式传参(适用于测试)三、运行时动态修改配置(不重启服务