【开源与项目实战:开源实战】82 | 开源实战三(中):剖析Google Guava中用到的几种设计模式

news2024/10/6 4:13:52

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

今天,我们再来学习一下,Google Guava 中用到的几种经典设计模式:Builder 模式、Wrapper 模式,以及之前没讲过的 Immutable 模式。

话不多说,让我们正式开始今天的学习吧!

Builder 模式在 Guava 中的应用

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

取而代之,我们可以在系统内部构建一个内存缓存,跟系统集成在一起开发、部署。那如何构建内存缓存呢?我们可以基于 JDK 提供的类,比如 HashMap,从零开始开发内存缓存。不过,从零开发一个内存缓存,涉及的工作就会比较多,比如缓存淘汰策略等。为了简化开发,我们就可以使用 Google Guava 提供的现成的缓存工具类 com.google.common.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.MINUTES)
            .build();
    cache.put("key1", "value1");
    String value = cache.getIfPresent("key1");
    System.out.println(value);
  }
}

从上面的代码中,我们可以发现,Cache 对象是通过 CacheBuilder 这样一个 Builder 类来创建的。为什么要由 Builder 类来创建 Cache 对象呢?我想这个问题应该对你来说没难度了吧。

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

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

那我再问你一个问题,为什么 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 模式的时候已经讲过了。现在,我们再结合 CacheBuilder 的源码重新说下。
必须使用 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> {
  protected ForwardingCollection() {
  }
  protected abstract Collection<E> delegate();
  public Iterator<E> iterator() {
    return this.delegate().iterator();
  }
  public int size() {
    return this.delegate().size();
  }
  @CanIgnoreReturnValue
  public boolean removeAll(Collection<?> collection) {
    return this.delegate().removeAll(collection);
  }
  public boolean isEmpty() {
    return this.delegate().isEmpty();
  }
  public boolean contains(Object object) {
    return this.delegate().contains(object);
  }
  @CanIgnoreReturnValue
  public boolean add(E element) {
    return this.delegate().add(element);
  }
  @CanIgnoreReturnValue
  public boolean remove(Object object) {
    return this.delegate().remove(object);
  }
  public boolean containsAll(Collection<?> collection) {
    return this.delegate().containsAll(collection);
  }
  @CanIgnoreReturnValue
  public boolean addAll(Collection<? extends E> collection) {
    return this.delegate().addAll(collection);
  }
  @CanIgnoreReturnValue
  public boolean retainAll(Collection<?> collection) {
    return this.delegate().retainAll(collection);
  }
  public void clear() {
    this.delegate().clear();
  }
  public Object[] toArray() {
    return this.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;
  }
  @Override
  protected Collection delegate() {
    return this.originalCollection;
  }
  @Override
  public boolean add(E element) {
    logger.info("Add element: " + element);
    return this.delegate().add(element);
  }
  @Override
  public boolean addAll(Collection<? extends E> collection) {
    logger.info("Size of elements to add: " + collection.size());
    return this.delegate().addAll(collection);
  }
}

结合源码和示例,我想你应该知道这组 Forwarding 类的作用了吧?
在上面的代码中,AddLoggingCollection 是基于代理模式实现的一个代理类,它在原始 Collection 类的基础之上,针对“add”相关的操作,添加了记录日志的功能。
我们前面讲到,代理模式、装饰器、适配器模式可以统称为 Wrapper 模式,通过 Wrapper 类二次封装原始类。它们的代码实现也很相似,都可以通过组合的方式,将 Wrapper 类的函数实现委托给原始类的函数来实现。

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

实际上,这个 ForwardingCollection 类是一个“默认 Wrapper 类”或者叫“缺省 Wrapper 类”。这类似于在装饰器模式那一节课中,讲到的 FilterInputStream 缺省装饰器类。你可以再重新看下第 50 讲装饰器模式的相关内容。
如果我们不使用这个 ForwardinCollection 类,而是让 AddLoggingCollection 代理类直接实现 Collection 接口,那 Collection 接口中的所有方法,都要在 AddLoggingCollection 类中实现一遍,而真正需要添加日志功能的只有 add() 和 addAll() 两个函数,其他函数的实现,都只是类似 Wrapper 类中 f2() 函数的实现那样,简单地委托给原始 collection 类对象的对应函数。

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

Immutable 模式在 Guava 中的应用

Immutable 模式,中文叫作不变模式,它并不属于经典的 23 种设计模式,但作为一种较常用的设计思路,可以总结为一种设计模式来学习。之前在理论部分,我们只稍微提到过 Immutable 模式,但没有独立的拿出来详细讲解,我们这里借 Google Guava 再补充讲解一下。
一个对象的状态在对象创建之后就不再改变,这就是所谓的不变模式。其中涉及的类就是不变类(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 针对集合类(Collection、List、Set、Map…)提供了对应的不变集合类(ImmutableCollection、ImmutableList、ImmutableSet、ImmutableMap…)。刚刚我们讲过,不变模式分为两种,普通不变模式和深度不变模式。Google Guava 提供的不变集合类属于前者,也就是说,集合中的对象不会增删,但是对象的成员变量(或叫属性值)是可以改变的。
实际上,Java JDK 也提供了不变集合类(UnmodifiableCollection、UnmodifiableList、UnmodifiableSet、UnmodifiableMap…)。那它跟 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"); // 抛出UnsupportedOperationException
    originalList.add("d");
    print(originalList); // a b c d
    print(jdkUnmodifiableList); // a b c d
    print(guavaImmutableList); // a b c
  }
  private static void print(List<String> list) {
    for (String s : list) {
      System.out.print(s + " ");
    }
    System.out.println();
  }
}

重点回顾

好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天我们学习了 Google Guava 中都用到的几个设计模式:Builder 模式、Wrapper 模式、Immutable 模式。还是那句话,内容本身不重要,你也不用死记硬背 Google Guava 的某某类用到了某某设计模式。实际上,我想通过这些源码的剖析,传达给你下面这些东西。

我们在阅读源码的时候,要问问自己,为什么它要这么设计?不这么设计行吗?还有更好的设计吗?实际上,很多人缺少这种“质疑”精神,特别是面对权威(经典书籍、著名源码、权威人士)的时候。

我觉得我本人是最不缺质疑精神的一个人,我喜欢挑战权威,喜欢以理服人。就好比在今天的讲解中,我把 ForwardingCollection 等类理解为缺省 Wrapper 类,可以用在装饰器、代理、适配器三种 Wrapper 模式中,简化代码编写。如果你去看 Google Guava 在 GitHub 上的 Wiki,你会发现,它对 ForwardingCollection 类的理解跟我是不一样的。它把 ForwardingCollection 类单纯地理解为缺省的装饰器类,只用在装饰器模式中。我个人觉得我的理解更加好些,不知道你怎么认为呢?

除此之外,在专栏的最开始,我也讲到,学习设计模式能让你更好的阅读源码、理解源码。如果我们没有之前的理论学习,那对于很多源码的阅读,可能都只停留在走马观花的层面上,根本学习不到它的精髓。这就好比今天讲到的 CacheBuilder。我想大部分人都知道它是利用了 Builder 模式,但如果对 Builder 模式没有深入的了解,很少人能讲清楚为什么要用 Builder 模式,不用构造函数加 set 方法的方式来实现。

课堂讨论

从最后一段代码中,我们可以发现,JDK 不变集合和 Google Guava 不变集合都不可增删数据。但是,当原始集合增加数据之后,JDK 不变集合的数据随之增加,而 Google Guava 的不变集合的数据并没有增加。这是两者最大的区别。那这两者底层分别是如何实现不变的呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/680257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【后端】使用TS编写任务管理系统----Express

文章目录 常见的后端框架安装并且声明文件库项目基本配置编写任务管理后端API添加任务查看任务设置任务完成状态删除任务 总结 node -v v16.13.0https://github.com/dL-hx/server-side 常见的后端框架 expresskoa… 安装并且声明文件库 $ npm i express $ npm i types/exp…

前端vue入门(纯代码)14

内容创作不易&#xff0c;各位帅哥美女&#xff0c;求个小小的赞&#xff01;&#xff01;&#xff01; 【15.给todoList案例添加编辑按钮】 本篇内容在TodoList案例的基础上添加个编辑按钮&#xff0c;要求&#xff1a; &#xff08;1&#xff09;点击编辑按钮后&#xff0c…

轻松学会研华屏幕下载和上传

&#x1f525;一个人走得远了&#xff0c;就会忘记自己为了什么而出发&#xff0c;希望你可以不忘初心&#xff0c;不要随波逐流&#xff0c;一直走下去&#x1f3b6; &#x1f98b; 欢迎关注&#x1f5b1;点赞&#x1f44d;收藏&#x1f31f;留言&#x1f43e; ✅ 如果觉得博主…

HashMap和HashSet的知识点总结

前言 在之前我们介绍过TreeMap和TreeSet&#xff1a; TreeMapTreeSet 知识点梳理总结_Crystal_bit的博客-CSDN博客 也知道Key-Value和Key模型&#xff0c;但是我们可能还对Hash不太了解&#xff0c;这里我们对Hash了解之后再对HashMap和HashSet的基本使用了解一下。 目录 1…

LabVIEW 图像处理功能

设置成像系统并采集图像后&#xff0c;您可以分析和处理图像&#xff0c;以提取有关被检测对象的有价值信息。 内容 图像分析图像处理斑点分析机器视觉 图像分析 影像分析结合了基于影像像素的灰度强度计算统计数据和测量的技术。您可以使用影像分析功能来确定影像质量是否足以…

scratch lenet(10): C语言计算log

scratch lenet(10): C语言计算log 文章目录 scratch lenet(10): C语言计算log1. 目的2. 原理: x m ∗ 2 p x m * 2^p xm∗2p3. 公式展开3.1 公式分解3.2 获取 m m m3.3 获取 p p p3.4 Remez 算法Remez 算法用于 sin(x) 的快速计算Remez 算法用于 exp 的近似Remez 用于自然…

【MySQL数据库】存储过程

目录 一、存储过程1.1概述1.2优点 二、存储过程实战2.1创建存储过程2.2存储过程的参数2.3条件语句 if-then-else end if2.4循环语句while end while 一、存储过程 1.1概述 存储过程是一组为了完成特定功能的SQL语句集合。存储过程在使用过程中是将常用或者复杂的工作预先使…

CSS基础学习--25 border(边框)进阶

一、边框常见属性 border-radiusbox-shadowborder-image 属性说明CSSborder-image设置所有边框图像的速记属性。3border-radius一个用于设置所有四个边框- *-半径属性的速记属性3box-shadow附加一个或多个下拉框的阴影3 二、border-radius 圆角 在 CSS2 中添加圆角棘手。我…

网络协议TCP/IP 协议学习笔记一

T C P / I P通常被认 为是一个四层协议系统&#xff0c;每一层负责不同的功能&#xff1a; 1) 链路层&#xff0c;有时也称作数据链路层或网络接口层&#xff0c; 通常包括操作系统中的设备驱动程序和计算机 中对应的网络接口卡。它们一起处理与电缆&#xff08;或其他任何传输…

CSS基础学习--26 渐变(Gradients)

CSS3 渐变&#xff08;gradients&#xff09;可以让你在两个或多个指定的颜色之间显示平稳的过渡。以前&#xff0c;你必须使用图像来实现这些效果。但是&#xff0c;通过使用 CSS3 渐变&#xff08;gradients&#xff09;&#xff0c;你可以减少下载的时间和宽带的使用。此外&…

Linux tracing之基于uprobe/kprobe的调试调优浅析

经过长期的发展, kprobes/uprobes 机制在事件(events)的基础上分别为内核态和用户态提供了追踪调试的功能, 这也构成了 tracepoint 机制的基础, 后期的很多工具, 比如 perf_events, ftrace 等都是在其基础上演化而来. 参考由 Brendan Gregg 提供的资料来看, kprobes/uprobes 在…

Minecraft我的世界服务器搭建之Linux系统,我的世界服务器推荐

Minecraft 是一个流行的沙箱独立游戏&#xff0c;由瑞典程序员 Markus “Notch” Perssion 首先创造&#xff0c;后来由 Mojang 开发并发布。这是一款关于打碎和放置砖块的游戏。首先&#xff0c;人们建造建筑物来抵抗夜晚的怪物&#xff0c;随着游戏的发展&#xff0c;玩家一起…

Spark Stream操作Kafka总结

kafka集群搭建 搭建参考 https://www.toutiao.com/article/6496743889053942286/?log_fromd5d6394cf75d_1687599146327 zk下载位置 国内&#xff1a;https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/ 国外&#xff1a;Apache ZooKeeper kafka位置 国内&#xff…

Kubernetes(k8s)容器编排Pod介绍和使用

目录 1 Pod 特点1.1 网络1.2 存储 2 使用方式2.1 自主式Pod2.2 控制器管理的Pod 3 自主运行Pod3.1 创建资源清单3.1.1 参数描述 3.2 创建Pod3.3 Pod操作3.3.1 查看Pod列表3.3.2 查看描述信息3.3.3 访问pod3.3.4 删除Pod 4 控制器运行Pod4.1 创建资源清单4.2 参数描述4.2.1 Repl…

【IDEA】Directory创建多级目录的正确写法

在resource下创建包的时候&#xff0c;右键resourcenew的时候并没有Package,只有Directory 我们也可以用Directory创建包&#xff0c;但写法与在Package下创建包的写法会不一样 例如&#xff1a; 在directory创建包 我们在去看文件的时候 如果是用&#xff08; com.dao.m…

【数据结构】树以及堆的讲解

(这里写自定义目录标题) 提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、树的概念&#xff1f;二、树的表示方法三、树的实际应用四、二叉树概念以及结构1.概念2.特殊的二叉树3.二叉树的性质4.二叉树的存储…

指针与数组--动态数组(2)[1、长度可变的一维动态数组 2、长度可变的二维动态数组]

目录 一、长度可变的一维动态数组 二、长度可变的二维动态数组 由上篇文章的理论&#xff0c;接下来使用例题来阐述。 一、长度可变的一维动态数组 例题1、编程输入某班学生的某门课成绩&#xff0c;计算并输出平均值。学生人数由键盘输入。 #include <stdio.h> #i…

Apache服务器

文章目录 Apache服务器Linux安装ApacheApache文件结构Apache主配置文件案例 配置一台Web服务器 启动用户的个人网站虚拟主机的设定基于IP的虚拟主机基于域名的虚拟主机基于端口的虚拟主机 rewrite重写rewrite使用详解使用案例 域名跳转单个域名跳转多个域名跳转 status状态页ap…

“插入排序:小数据量排序的王者“

文章目录 &#x1f50d;什么是插入排序&#xff1f;&#x1f511;插入排序的优缺点&#x1f680;实现插入排序 &#x1f50d;什么是插入排序&#xff1f; 插入排序是一种简单的排序算法&#xff0c;它的基本思想是&#xff1a;将待排序的元素&#xff0c;从第二个元素开始&…

阿里架构师整理的Java经典面试题1220道(附答案)

学习如逆水行舟&#xff0c;尤其是 IT 行业有着日新月异的节奏&#xff0c;我们更要抓紧每一次可以学习和进步的机会。所以&#xff0c;没有撤退可言 即使是面试跳槽&#xff0c;那也是一个学习的过程。只有全面的复习&#xff0c;才能让我们更好的充实自己&#xff0c;武装自…