概述
上篇文章,我通过 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 针对集合(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"); // 抛出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();
}
}