在看到这个Item时,第一反应:什么是依赖注入?什么是硬连接?分别是怎么实现的?为什么前者优于后者?
背景
许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态工具类并不少见 :
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) { ... }
}
函数的行为由某个参数决定时,考虑多对象,用此参数作为建立对象的依据。
硬连接资源
上面使用的方式都是硬连接(硬编码);什么是硬连接,我在此处的理解是在代码中直接引用某个固定的资源,这样可以保证一个类对底层资源的依赖吗?当然可以!但是问题在于如果一个类依赖多种资源呢?以上面的例子为例,我们引用的字典资源可能是中文字典,也可能是英文字典,每次资源发生变化时都需要我们在代码中去修改,这样的方式显然不够灵活。
也许你早已想到了我们可以在类需要字典资源的时候再去指定,例如将属性设置为非 final ,再提供一个方法去修改资源,effective java中说这样的设置显得非常笨拙、容易出错、并且无法并行工作。静态工具类和单例类不适合与需要引用底层资源的类。
我们常见的还有使用配置文件去指定业务代码某个属性的值,这就和硬连接不同了,随之引出我们接下来要谈到的依赖注入。
依赖注入
满足这一需求的简单模式是,在创建新实例时将资源传递到构造器中。 这是依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
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 pattern)[Gamma95]。 Java 8 中引入的 Supplier<T> 接口非常适合代表工厂。 在输入上采用 Supplier<T> 的方法通常应该使用有界的通配符类型(bounded wildcard type)(详见第 31 条)约束工厂的类型参数,以便客户端能够传入一个工厂,来创建指定类型的任意子类型。例如,下面是一个生产马赛克的方法,它利用客户端提供的工厂来生产每一片马赛克:
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
尽管依赖注入极大地提高了灵活性和可测试性,但它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。使用依赖注入框架(如 Dagger [Dagger]、Guice [Guice] 或 Spring [Spring])可以消除这些混乱。这些框架的使用超出了本书的范围,但是请注意,为手动依赖注入而设计的 API 非常适合这些框架的使用。
总结
总而言之,不要用单例和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注人,它极大地提升了类的灵活性、可重用性和可测试性。
|