很多类依赖于一个或多个底层资源(underlying resources)。比如,一个拼写检查器依赖一本词典。这种类被实现为静态工具类的情况并不少见(第 4 条):
// 静态工具类的不恰当使用 - 不灵活 & 不可测试!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 不可实例化
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
同样,它们被实现为单例的情况也很常见:
// 单例的不恰当使用 - 不灵活 & 不可测试!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
这两种方式都不尽人意,因为他们假设只有一本字典会被使用。事实上,每种语言都有它自己的字典,并且特定的词汇需要特定的字典。同样,用一本特殊的字典来测试也是我们所期望的。妄想使用一本字典满足所有的需求只是一厢情愿而已。
为了使 SpellChecker
支持多本字典,你可能会想到通过去掉 dictionary
的 final
属性,并添加一个改变现有 SpellChecker
中 dictionary
属性的方法来实现,然而在并发环境下,这种方式显得笨拙,更容易出错导致程序终止。行为被底层资源参数化的类不适合作为静态工具类或单例。
我们需要的是类具有支持多个实例的能力(在我们的例子中是 SpellChcker
),并且在每个实例中使用客户端所需的资源(在我们例子中是 dictionary
)。满足这一需求的简单方法是在创建新的实例时,将资源传递到构造器中。这是依赖注入(dependency injection)的一种形式:拼写检查器所依赖的字典在 SpellChecker
对象被创建时注入到其构造方法中。
// 依赖注入提供了灵活性和可测性
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
依赖注入模式如此简单,以至于很多程序员常年都在使用却不知道它还有个名字。虽然我们的拼写检查器的例子只依赖于一个资源(dictionary
),实际上依赖注入可以支持任意数量的资源和依赖图(dependency graph)。它保证了底层资源的不可变性(第 17 条),所以多个客户端可以共享依赖对象(假如客户端申请同一个底层资源)。依赖注入同样适用于构造器,静态工厂和构建器(第 2 条)等。
依赖注入模式的一个实用变体是给构造器传递一个资源工厂(resource factory),即创建类型实例时可以被反复调用的对象。像这样的工厂体现了工厂方法模式(factory method pattern)。JAVA 8 中引入的 Supplier<T>
接口非常适合表示工厂。当一个方法使用 Supplier<T>
作为输入参数时,应该通过使用边界通配符类型(bound wildcard type 第 31 条)限制其工厂的类型参数,这样客户端就可以传入创建指定类型的子类型的工厂。比如下面这个例子,使用客户端提供的工厂来生产 tile (瓷砖),从而制作 mosaic(马赛克):
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
尽管依赖注入大大提高了程序的灵活性和可测试性,但它可能会使通常包含数千个依赖项的大型项目变得混乱 。这种混乱可以通过使用依赖注入框架(dependency injection framework)来消除,比如 Dagger,Guice 或 Spring。本书不讨论这些框架的用法,但请注意,这些框架非常适用于为手动实现依赖注入设计的 API。
总而言之,不要使用单例或静态工具类来实现那些依赖于一个或多个底层资源且其行为会被资源影响的类,也不要让类直接创建这些资源。相反,应该把资源或创建资源的工厂传递到构造器(或静态工厂或构建器)中。依赖注入的使用会大大提高类的灵活性、可重用性和可测试性。
翻译:Inger
校正:Angus