Effective Java 3rd 条目5 依赖注射优于硬耦合
很多类依赖于一个或者多个相关资源。比如,一个拼写检测器依赖一个字典。这样的类实现为静态效用类,这是很常见的(条目4):
// 不恰当的静态效用的使用 - 不灵活而且不可测试!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 不可实例化化
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
相似地,这些类实现为单例,这也是很常见的(条目3):
// 不恰当的单例的使用 - 不灵活而且不可测试!
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支持多个字典:字典的域为非final,然后在目前的拼写检测器中添加一个改变字典的方法。但是这个可能很笨拙,容易出错,而且在多线程环境不能工作。静态效用类和单例对于这样的类是不恰当的:它的行为是由相关资源参数化。
要求的是这种能力:支持这个类的多个实例(在我们的例子中,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) { ... }
}
依赖注射是如此简单,以致许多程序员多年使用而不知道它的名字。虽然我们的拼写检测器例子仅仅只有单一资源(即,字典),依赖注射对于任意资源数量和任意依赖图谱也起作用。它保持不可变性(条目17),所以多个客户端可以分享依赖对象(假设客户端需要同样的相关资源)。依赖注射同样可以应用到构造子,静态工厂(条目1)和builder(条目2)。
这个模式的一个有用变体是,把资源工厂传递给构造子。工厂是一个对象,可以重复调用创建一种类型的实例。这样的工厂具体表现为工厂方法模式(Factory Method)[Gamma95]。Java 8引入的Supplier<T>接口完美代表工厂。输入有Supplier<T>的方法,往往用受限的通配类型(bounded wildcard type)(条目31)来限制工厂类型参数,这让客户端传入一个工厂,这个工厂可以创建指定类型的任何子类型。比如,这里有个方法,用客户端提供的工厂生成每个地砖来铺设镶嵌地砖。
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
尽管依赖注射极大提高了灵活性和可测试性,但是它使得大项目凌乱,大项目往往包含成千个依赖。用依赖注射框架(∫dependency injection framework)可以消除凌乱,比如Dagger[Dagger]、Guice[Guice]或者Spring[Spring]。使用这些框架不在这本书的范围,但是记住,为手动依赖注入设计的API,可以用这些框架轻松适配。
总之,不要用单例或者静态效用类实现这样的类,这个类依赖一个或者多个相关资源,而这些资源的行为影响了这个类。不要用这个类直接创建这些资源。相反,传递资源或者工厂到构造子来创建它们(或者静态工厂或者builder)。这个实践,叫做依赖注射,大大的增强了一个类的灵活性、重用性和可测试性。