Effective Java 第三版读书笔记——条款5:使用依赖注入替代替代硬连接资源
许多类都会依赖一个或多个基本资源。例如,拼写检查器依赖于字典。下面是两种错误的实现方式:
-
使用 static utility classes:
// Inappropriate use of static utility - inflexible & untestable! public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() {} // Noninstantiable public static boolean isValid(String word) { ... } public static List<String> suggestions(String typo) { ... } }
-
使用 singletons:
// Inappropriate use of singleton - inflexible & untestable! 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) { ... } }
这两种方法都不令人满意,因为他们假设只有一本字典值得使用。在实际中,每种语言都有自己的字典,特殊的字典被用于特殊的词汇表。另外,使用专门的字典来进行测试也是可取的。因此不能想当然地认为一本字典就足够了。
可以通过将 dictionary
属性设置为非 final
,并添加一个方法来更改现有拼写检查器中的字典,从而让拼写检查器支持多个字典,但是在并发环境中,这是笨拙的、容易出错的和不可行的。
我们需要的是能够支持多个实例的类(即示例中的 SpellChecker
),每个实例都使用客户端所期望的资源(即示例中的 dictionary
)。满足这一需求的简单模式是在创建新实例时将资源传递到构造器中。这是依赖注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当拼写检查器创建时它会被注入其中。
// Dependency injection provides flexibility and testability
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)。
尽管依赖注入极大地提高了灵活性和可测试性,但它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。使用依赖注入框架(如 Dagger、Guice 或 Spring)可以消除这些混乱。