设计模式学习笔记 - 开源实战三(中):剖析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 针对集合(Collection、List、Set、Map…)提供了对应地不变集合类(ImmutableCollection、ImmutableList、ImmutableSet、ImmutableMap…)。刚刚讲过,不变模式分为两种,普通不变模式和深度不变模式。Google Guava 提供的不变集合类属于前者,也就是说集合中的对象不会增删,但对象的成员变量是可以改变的。

实际上,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"); // 抛出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.tpcf.cn/pingmian/1105.html

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

相关文章

【Hadoop3.3.6全分布式环境搭建】

说明: 完成Hadoop全分布式环境搭建,需准备至少3台虚拟机(master slave01 slave02)环境: VMWare + Centos7 + JDK1.8+ Hadoop3.3.6主机规划: 主节点:master从节点:slave01 , slave02 一、准备工作 1、所有主机安装jdk 上传jdk-8u171-linux-x64.tar.gz到/root目录下,然后…

AWS SAP(Certified Solutions Architect- Professional)认证题库详解(一)

很多朋友在做AWS认证的时候拿到题库之后,却不理解正确答案的原理,这里针对每一条答案都给了一个非常详细的解释,同时也会积累丰富的云计算架构的实践经验,保证看得懂,记得住,看完一遍之后就可以去考试了。  问题1 一家公司使用Amazon在单个m4.2xlarge AmazonEC2实例上…

改变图片的DPI和 清晰度

投稿时&#xff0c;我们的图片不满足要求&#xff0c;经常需要修改&#xff0c;这个博主讲解了几种方法&#xff0c;非常好。 【科研制图】如何快速修改图片符合投稿要求(分辨率300DPI等)及AI智能绘制高质量图片&#xff08;Upscayl软件&#xff09;_哔哩哔哩_bilibili

【java】26:JUnit

为什么需要 JUnit&#xff1a; 1.一个类有很多功能代码需要测试&#xff0c;为了测试&#xff0c;就需要写入到main方法中 2.如果有多个功能代码测试&#xff0c;就需要来回注销&#xff0c;切换很麻烦……… 3.如果可以直接运行一个方法&#xff0c;就方便很多&#xff0c;并且…

openssl3.2 - exp - 用base64后的字符串作为配置项的值

文章目录 openssl3.2 - exp - 用base64后的字符串作为配置项的值概述笔记配置项的值长度有限制 配置项的值不能是base64之后的直接值&#xff0c;需要处理之后才行。openssl配置项的值并不是所有可见字符都可以例子现在用的base64的类cipher_base64.hcipher_base64.cpp 现在用的…

ChatGPT研究论文提示词集合1-【主题选择与问题研究、文献综述】

点击下方▼▼▼▼链接直达AIPaperPass &#xff01; AIPaperPass - AI论文写作指导平台 目录 1.主题选择与问题定义 2.文献综述 3.书籍介绍 AIPaperPass智能论文写作平台 近期小编按照学术论文的流程&#xff0c;精心准备一套学术研究各个流程的提示词集合。总共14个步骤…

论文笔记:Large Language Model for Participatory Urban Planning

202402 arxiv 大模型城市规划 引入了一个基于LLM的多代理协作框架&#xff0c;模拟规划师和数千名具有不同特征和背景的居民&#xff0c;用于参与式城市规划——>生成考虑居民多样化需求的城市区域土地利用规划为了提高讨论的效率&#xff0c;论文采用了鱼缸讨论机制&#…

python——函数

概念 函数就是将一段具有独立功能的代码块整合到一个整体并命名&#xff0c;在需要的位置调用这个名称去完成对应的需求 定义函数 def 函数名&#xff08;参数&#xff09;&#xff1a;代码1......return 值函数名 函数名由编程人员自定义的&#xff0c;满足标识符命名规则…

建筑楼宇VR火灾扑灭救援虚拟仿真软件厂家

在传统消防安全教育方式中&#xff0c;往往存在内容枯燥、参与度低和风险大等问题&#xff0c;使得消防安全知识难以深入人心。然而&#xff0c;借助VR消防安全逃生教育系统&#xff0c;我们可以打破这一困境&#xff0c;为公众带来前所未有的学习体验。 VR消防安全逃生教育系统…

【Web】DASCTF X GFCTF 2024|四月开启第一局 题解

目录 EasySignin cool_index web1234 web4打破防了&#x1f92e;&#xff0c;应该很接近解出来了&#xff0c;感兴趣的师傅续上吧 EasySignin 先随便注册个账号登录&#xff0c;然后拿bp抓包改密码(username改成admin) 然后admin / 1234567登录 康好康的图片功能可以打SS…

OEEL图表——饼状图绘制(各国太阳能发电占比)

简介 本数据主要的目的就是进行饼状图的绘制,所使用的数据全球电力系统分布情况 数据属性 NameTypeDescriptioncountryString3-character country code corresponding to the ISO 3166-1 alpha-3 specscountry_lgStringLonger form of the country designationnameStringNam…

数据采集技术综合项目实战3(网络爬虫+数据预处理+数据可视化)附带详细步骤说明,干货满满

项目介绍及需求&#xff1a; 本项目主要是通过对b站电影弹幕进行采集并分析。1.获得弹幕高频词生成符合该电影特征、主题、角色等相关字段的词云图&#xff0c;通过词云图的方式对某部电影主题具体化。2.获取用户年内评论发布时间观生成时间的折线图&#xff0c;以便从侧面观察…

在Vue项目使用kindEditor富文本编译器以及上传图片

第一步 npm install kindeditor第二步&#xff0c;建立kindeditor.vue组件 <template><div class"kindeditor"><textarea :id"id" name"content" v-model"outContent"></textarea></div> </templa…

Android中logcat日志、ANR日志、trace日志的介绍与分析方法

Logcat日志 在Android开发中&#xff0c;日志是开发者在应用程序中添加的一种输出信息的记录方式&#xff0c;用于查看应用程序在运行时的状态、调试信息、错误信息等。Android日志可以帮助开发者追踪应用程序的行为、诊断问题并进行调试。Android提供了一系列的日志输出方法&…

使用 vllm 运行 Llama3-8b-Instruct

使用 vllm 运行 Llama3-8b-Instruct 0. 引言1. 安装 vllm2. 运行 Llama3-8b-Instruct 0. 引言 此文章主要介绍使用 vllm 运行 Llama3-8b。 1. 安装 vllm 创建虚拟环境&#xff0c; conda create -n myvllm python3.11 -y conda activate myvllm安装 Ray 和 Vllm&#xff0c…

富格林:致用查明暗箱黑幕技巧

富格林认为&#xff0c;投资现货黄金对于新手投资者来说是一个很好的选择&#xff0c;但是在进行投资之前需要了解一些基本的技巧和策略用以查明暗箱黑幕。事实上&#xff0c;现货黄金市场充满着丰富的交易机会&#xff0c;以及并存的交易风险&#xff0c;因此投资者要想在这其…

动态规划——状态压缩dp

acwing291.蒙德里安的梦想 #include<iostream> #include<cstring>using namespace std;const int N 12, M 1 << N;long long int f[N][M]; //f[i][j]表示第i列j状态有多少个可行解 int n, m; bool st[M]; //st[i]表示合并列的状态i是否合法int main() {wh…

10分钟快速部署 Doris 集群

部署 Doris 集群通常涉及多个步骤&#xff0c;包括下载和解压软件包、配置 FE 和 BE 节点以及启动服务。下面是在三台 Linux 服务器上部署一个包含三个 FE (Frontend) 和三个 BE (Backend) 的 Doris 集群的详细步骤&#xff1a; 服务器ip&#xff1a;192.168.0.103、192.168.0.…

02_补充)docker学习 docker在线安装tomcat等,docker容器常用指令

01中安装好docker后&#xff0c;本章学习使用docker拉取tomcat&#xff0c;nginx&#xff0c;jdk等镜像&#xff0c;并创建容器启动测试 常用命令 # 基本信息查看 docker version # 查看docker的版本号&#xff0c;包括客户端、服务端、依赖的Go等 docker info # 查看系统(…

【vue3】插件@tsparticles/vue3、tsparticles实现粒子特效

文章目录 一、安装依赖二、全局引入三、使用 一、安装依赖 npm i tsparticles/vue3 npm i tsparticles二、全局引入 // main.js import Particles from tsparticles/vue3 import { loadFull } from tsparticlesconst app createApp(App) app.use(Particles, {init: async (e…