设计模式学习笔记 - 开源实战三(中):剖析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();}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

稀碎从零算法笔记Day54-LeetCode:39. 组合总和

题型&#xff1a;数组、树、DFS、回溯 链接&#xff1a;39. 组合总和 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数…

卷王问卷考试系统/SurveyKing调查系统源码

SurveyKing是一个功能强大的开源调查问卷和考试系统&#xff0c;它能够快速部署并适用于各个行业。 这个系统提供了在线表单设计、数据收集、统计和分析等功能&#xff0c;支持20多种题型&#xff0c;提供多种创建问卷的方式和设置。 项 目 地 址 &#xff1a; runruncode.c…

软件设计师软考中项学习(二)之计算机系统基础知识

读者大大们好呀&#xff01;&#xff01;!☀️☀️☀️ &#x1f525; 欢迎来到我的博客 &#x1f440;期待大大的关注哦❗️❗️❗️ &#x1f680;欢迎收看我的主页文章➡️寻至善的主页 文章目录 学习目标学习内容学习笔记学习总结 学习目标 计算机系统硬件基本组成 中央处理…

React【Day4】

路由快速上手 1. 什么是前端路由 一个路径 path 对应一个组件 component 当我们在浏览器中访问一个 path 的时候&#xff0c;path 对应的组件会在页面中进行渲染 2. 创建路由开发环境 # 使用CRA创建项目 npm create-react-app react-router-pro# 安装最新的ReactRouter包 …

【6】mysql查询性能优化-关联子查询

【README】 0. 先说结论&#xff1a;一般用inner join来改写in和exist&#xff0c;用left join来改写not in&#xff0c;not exist&#xff1b;&#xff08;本文会比较内连接&#xff0c;包含in子句的子查询&#xff0c;exist的性能 &#xff09; 1. 本文总结自高性能mysql 6…

Spark-机器学习(3)回归学习之线性回归

在之前的文章中&#xff0c;我们了解我们的机器学习&#xff0c;了解我们spark机器学习中的特征提取和我们的tf-idf&#xff0c;word2vec算法。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你…

异常检测 | SVDD支持向量数据描述异常数据检测(Matlab)

异常检测 | SVDD支持向量数据描述异常数据检测&#xff08;Matlab&#xff09; 目录 异常检测 | SVDD支持向量数据描述异常数据检测&#xff08;Matlab&#xff09;效果一览基本介绍程序设计参考资料 效果一览 基本介绍 用于一类或二元分类的 SVDD 模型 多种核函数&#xff08;…

栈与堆的比较

栈与堆的比较 栈与堆的比较申请后系统的响应申请效率的比较申请大小的限制堆和栈中的存储内容总结参考 栈与堆的比较 内存布局&#xff1a; 申请后系统的响应 栈&#xff1a;只要栈的剩余空间大于所申请空间&#xff0c;系统将为程序提供内存&#xff0c;否则将报异 常提示…

InnoDB架构:磁盘篇

InnoDB架构&#xff1a;磁盘篇 InnoDB是MySQL数据库中默认的存储引擎&#xff0c;它为数据库提供了事务安全型&#xff08;ACID兼容&#xff09;、行级锁定和外键支持等功能。InnoDB的架构设计优化了对于读取密集和写入密集型应用的性能表现&#xff0c;是一个高度优化的存储系…

【经典小游戏】猜数字

前言1. 游戏介绍2. 游戏实现3. 游戏优化结语 个人主页&#xff1a;C_GUIQU 前言 各位小伙伴大家好&#xff01; 先问大家一个问题&#xff1a;我们为什么要学习&#xff1f; 简单来说&#xff0c;就是为了实践&#xff01;只有不断学习才可以帮助我们更好地实践&#xff01; 小…

驱动开发-windows驱动设计目标

驱动程序和应用程序不一样的&#xff0c;由于其直接运行于windows r0级&#xff0c;故对于开发有更多和更严格的标准&#xff0c;一般会有以下一些常见的设计目标: 安全性、可移植性、可配置性、 可被中断、多处理器安全、可重用 IRP、 支持异步 I/O这些是基本目标。 1. 安全…

稀碎从零算法笔记Day53-LeetCode:不同路径 II

稀碎系列有点更不动(更多是自己懈怠了) 题型&#xff1a;矩阵、模拟 链接&#xff1a;63. 不同路径 II - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &…