美文网首页
82 - 剖析Google Guava中用的设计模式(二)

82 - 剖析Google Guava中用的设计模式(二)

作者: 舍是境界 | 来源:发表于2021-10-21 06:56 被阅读0次

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

    Builder 模式在 Guava 中的应用

    • 常用的缓存系统有 Redis、Memcache 等。但是,如果要缓存的数据比较少,我们完全没必要在项目中独立部署一套缓存系统。毕竟系统都有一定出错的概率,项目中包含的系统越多,那组合起来,项目整体出错的概率就会升高,可用性就会降低。同时,多引入一个系统就要多维护一个系统,项目维护的成本就会变高。
    • 取而代之,我们可以在系统内部构建一个内存缓存,跟系统集成在一起开发、部署。那如何构建内存缓存呢?我们可以基于 JDK 提供的类,比如 HashMap,从零开始开发内存缓存。不过,从零开发一个内存缓存,涉及的工作就会比较多,比如缓存淘汰策略等。为了简化开发,我们就可以使用 Google Guava 提供的现成的缓存工具类 com.google.common.cache.*。
    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() 方法来逐一设置需要的设置的成员变量。
    • 为什么不用第二种,我们看下源码就清楚了
    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开头部分类
    • 这组 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();
      }
      
      //...省略部分代码...
    }
    
    • 再举一个它的用法示例,如下所示:
    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);
      }
    }
    
    • 在上面的代码中,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 缺省装饰器类
    • 如果我们不使用这个 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 模式。通过学习设计模式能让你更好的阅读源码、理解源码。

    相关文章

      网友评论

          本文标题:82 - 剖析Google Guava中用的设计模式(二)

          本文链接:https://www.haomeiwen.com/subject/qvnwoltx.html