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

概述

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

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


Builder 模式在 Google Guava 中的应用

在项目开发中,我们常用到缓存,它可以有效地提高访问速度。

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

取而代之,我们可以在系统内部构件一个内存缓存,跟系统集成在一起开发、部署。那如何构建内存缓存呢? 我们可以基于 JDK 提供的类。比如 HashMap ,从零开始开发内存缓存。不过,从零开发一个缓存,涉及的工作会比较多,比如缓存淘汰策略等。为了简化开发,我们就可以使用 Google Guava 提供的线程的缓存工具类 com.google.connom.cache.*

使用 Google Guava 来构建内存缓存非常简单,下面是我我写的一个例子。

java 复制代码
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() 函数摘抄到了下面。

java 复制代码
    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 类是干什么用的。

java 复制代码
@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 的代码实现,你可能想不到它的作用。下面是一个它的用法示例。

java 复制代码
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 类的函数实现委托给原始类的函数来实现。

java 复制代码
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)。

  • 普通不变模式指的是,对象中包含的引用对象是可变的。如果不特别说明,通常我们所说的不变模式,指的就是普通的不变模式。
  • 深度不变模式指的是,对象包含的引用对象也不可能。

它们之间的关系,有点类似之前讲过的浅拷贝和深拷贝之间的关系。下面是一个示例代码:

java 复制代码
// 普通不变模式
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 提供的不便集合类的区别在哪里呢?我举个例子就明白,代码如下所示:

java 复制代码
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();
    }
}