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

news2024/10/6 6:03:04

概述

上篇文章,我通过 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() {}

  @Override
  protected abstract Collection<E> delegate();

  @Override
  public Iterator<E> iterator() {
    return delegate().iterator();
  }

  @Override
  public int size() {
    return delegate().size();
  }

  @CanIgnoreReturnValue
  @Override
  public boolean removeAll(Collection<?> collection) {
    return delegate().removeAll(collection);
  }

  @Override
  public boolean isEmpty() {
    return delegate().isEmpty();
  }

  @Override
  public boolean contains(Object object) {
    return delegate().contains(object);
  }

  @CanIgnoreReturnValue
  @Override
  public boolean add(E element) {
    return delegate().add(element);
  }

  @CanIgnoreReturnValue
  @Override
  public boolean remove(Object object) {
    return delegate().remove(object);
  }

  @Override
  public boolean containsAll(Collection<?> collection) {
    return delegate().containsAll(collection);
  }

  @CanIgnoreReturnValue
  @Override
  public boolean addAll(Collection<? extends E> collection) {
    return delegate().addAll(collection);
  }

  @CanIgnoreReturnValue
  @Override
  public boolean retainAll(Collection<?> collection) {
    return delegate().retainAll(collection);
  }

  @Override
  public void clear() {
    delegate().clear();
  }

  @Override
  public 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;
    }

    @Override
    protected Collection<E> 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);
    }
}

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

前面讲过,代理模式、装饰器、适配器模式都可以成为 Wapper 模式,通过 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 originalClass;
    public WrapperClass(OriginalClass originalClass) {
        this.originalClass = originalClass;
    }
    @Override
    public void f1() {
        // 附加功能...
        originalClass.f1();
        // 附加功能...
    }
    @Override
    public 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"); // 抛出UnsupportedOperationException
        originalList.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.coloradmin.cn/o/1611358.html

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

相关文章

Vue3——组件基础

组件基础 1. 组件定义与使用 1.1 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>组件基础&l…

力扣-LCP 02.分式化简

题解&#xff1a; class Solution:def fraction(self, cont: List[int]) -> List[int]:# 初始化分子和分母为 0 和 1n, m 0, 1# 从最后一个元素开始遍历 cont 列表for a in cont[::-1]:# 更新分子和分母&#xff0c;分别为 m 和 (m * a n)n, m m, (m * a n)# 返回最终的…

程序员如何应对久坐带来的不良影响

久坐是程序员工作中常见的问题&#xff0c;它可能对身体健康产生多种负面影响。为了应对这些影响&#xff0c;程序员可以采取以下措施&#xff1a; 定时休息&#xff1a;每隔45分钟至1小时&#xff0c;起身活动5-10分钟。这可以帮助缓解长时间坐姿带来的身体压力&#xff0c;促…

YOLOv9改进策略 | 添加注意力篇 | 挤压和激励单元SENetV2助力YOLOv9细节涨点(全网独家首发)

一、本文介绍 本文给大家带来的改进机制是SENetV2&#xff0c;其是一种通过调整卷积网络中的通道关系来提升性能的网络结构。SENet并不是一个独立的网络模型&#xff0c;而是一个可以和现有的任何一个模型相结合的模块(可以看作是一种通道型的注意力机制但是相对于SENetV1来说…

Aws Nat Gateway

要点 NAT网关要能访问外网&#xff0c;所以需要部署在有互联网网关的Public子网中。 关键&#xff1a; NAT网关创建是选择子网&#xff0c;一定要选择公有子网&#xff08;有互联网网关子网&#xff09; 特别注意&#xff1a; 新建nat网关的时候&#xff0c;选择的子网一定…

Ubuntu无法安装向日癸15.2.0.63062_amd64.deb最新版

Ubuntu安装向日葵远程控制 安装包下载 安装方式 方式一&#xff1a;运行安装包安装 方式二&#xff1a;终端命令安装 通过以下教程可以快速的安装向日葵远程控制&#xff0c;本教程适用于Ubuntu18.04/20.04/22.04 安装包下载 进入向日葵远程控制下载官网下载向日葵远程控制Lin…

使用Python+opencv实现自动扫雷

大家好&#xff0c;相信许多人很早就知道有扫雷这么一款经典的游戏&#xff0c;更是有不少人曾听说过中国雷圣&#xff0c;也是中国扫雷第一、世界综合排名第二的郭蔚嘉的顶顶大名。扫雷作为一款在Windows9x时代就已经诞生的经典游戏&#xff0c;从过去到现在依然都有着它独特的…

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

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

安居水站:水站经营秘籍:年入30万不是梦。水站创业指南。

在这个快节奏的社会里&#xff0c;初创企业家们总是在寻找一条明路&#xff0c;以在竞争激烈的市场中立足。为了帮助他们更好地实现这一目标&#xff0c;我根据经验决定制定一份水站经营指导手册。这份手册将详细阐述如何从零起步&#xff0c;如何运营&#xff0c;如何进行市场…

开源博客项目Blog .NET Core源码学习(16:App.Hosting项目结构分析-4)

本文学习并分析App.Hosting项目中前台页面的文章专栏页面和文章详情页面。< 文章专栏页面 文章专栏页面总体上为左右布局&#xff0c;左侧显示文章列表&#xff0c;右侧从上向下为关键词搜索、分类导航、热门文章等内容。整个页面使用了layui中的面包屑导航、表单、模版、流…

【C++初阶】List使用特性及其模拟实现

1. list的介绍及使用 1.1 list的介绍 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前…

利用OpenCV4.9制作自己的线性滤波器!

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV4.9使用 inRange 的阈值操作 下一篇 :OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 f…

Android JetPack Compose+Room----实现搜索记录功能

文章目录 需求概述功能展示实现搜索功能使用的技术1.Android Jetpack room2.Android JetPack Compose 代码实现编写搜索界面接入Room实现搜索功能的管理引入依赖定义包结构定义操作表的Dao类定义数据库的基础配置定义数据库的Dao管理类使用数据库升级 源码地址 需求概述 搜索功…

java:Java中的抽象类

什么是抽象类&#xff1a; 我们知道&#xff0c;类用来模拟现实的事物&#xff0c;一个类模拟一类事物&#xff0c;某个类的一个实例化对象可以模拟某个属于该类的具体事物。类中描绘了该类所有对象的共同的特性&#xff0c;当一个类中给出的信息足够全面时候&#xff0c;我们就…

算法练习|Leetcode189轮转数组 ,Leetcode56合并区间,Leetcode21合并两个有序链表,Leetcode2两数相加,sql总结

目录 一、Leetcode189轮转数组题目描述解题思路方法:切片总结 二、Leetcode56合并区间题目描述解题思路方法:总结 三、Leetcode21合并两个有序链表题目描述解题思路方法:总结 四、Leetcode2两数相加题目描述解题思路方法:总结 sql总结: 一、Leetcode189轮转数组 题目描述 给定…

【GIS教程】ArcGIS做日照分析(附练习数据下载)

我国对住宅日照标准的规定是:冬至日住宅底层日照不少于1小时或大寒日住宅层日照不少于2小时(通常以当地冬至日正午12时的太阳高度角作为依据)。因冬至日太阳高度角最低&#xff0c;照射范围最小&#xff0c;如果冬至日12&#xff1a;00建筑物底层能够接收到阳光&#xff0c;那么…

Go语言中通过数据对齐降低内存消耗和提升性能

数据对齐是一种安排数据分配方式以加速 CPU 访问内存的方法。 不了解这个概念会导致额外的内存消耗甚至性能下降。 要了解数据对齐的工作原理&#xff0c;让我们首先讨论没有它会发生什么。假设我们分配两个变量&#xff0c;一个 int32 类型的 &#xff08;32 B&#xff09; 和…

OJ:数字三角形(搜索)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;每日一练 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f337;1.问题描述&#xff1a; ⛳️题目描述&#xff1a; 示出了一个数字三角形。 请编一个程序计算从顶至底的某处的一条路…

指针的使用以及运算、二级指针、造成野指针的原因以及解决方法、指针和数组相互使用

第七章&#xff0c;指针的学习 目录 前言 一、指针的概念 二、指针的类型 三、野指针 四、指针的运算 五、指针和数组的关系以及使用 六、指针数组 七、二级指针 总结 前言 这章主要学习的是指针方面的知识&#xff0c;这节只是简单了解一下指针&#xff0c;并不会深…

使用HTML和CSS和PHP实现一个简单的简历制作项目

实 验 目 的 掌握HTML表单作用&#xff0c;以及action和method属性&#xff1b; 掌握HTML输入域作用、类型、标签&#xff0c;以及name和value属性&#xff1b; 掌握$_REQUEST变量的作用、语法和使用&#xff1b; 掌握注释&#xff0c;以及变量的作用、命名、赋值和输出&#…