在日常项目中, 许多类可能依赖一些底层资源
比如说拼写检查器依赖于字典, 可能会将此类作为静态工具类实现
public class SpellChecker {
private static final Dictionary dictionary = ...;
// 不需要实例化
private SpellChecker() {}
// 检查是否拼写对
public static Boolean validate(String word) {..}
// 根据错别字, 返回字典中相关拼写的字
public static List<String> suggest(String typo) {...}
}
这种方法并不怎么好, 因为在SpellChecker
的代码中已经依赖了Dictionary
类的具体实现, 类与类之间已经产生了耦合, 可能目前系统只需要检查英文拼写, 现在还能使用, 但是如果以后增加了扩展, 比如说需要支持中文拼写, 则只能将dictionary
声明成非final
的并提供一个set方法来设置, 如下段代码
public static void setDictionary(Dictionary dictionary) {
SpellChecker.dictionary = dictionary;
}
但这样的做法不好的地方在于, 它使得dictionary
变量变成了非final的(可变的), 使该变量可以重复赋值, 安全性降低, 可能代码中不小心set了好几次dictionary
而造成了意想不到的结果
满足这一需求的简单模式是在创建新实例时将资源传递到构造方法中。这是依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
public class SpellChecker {
// dictionary只能被赋值一次
private final Dictionary dictionary;
public SpellChecker(Dictionary dictionary) {
this.dictionary = dictionary;
}
// 验证是否拼写对
public Boolean validate(String word) {...}
// 根据错别字, 返回字典中相关拼写的字
public List<String> suggest(String typo) {...}
}
依赖注入同样适用于构造方法,静态工厂和 builder模式。
该模式的一个有用的变体是将资源工厂传递给构造方法。
工厂是可以重复调用以创建类型实例的对象。 这种工厂体现了工厂方法模式(Factory Method pattern )。
Java 8中引入的Supplier <T>接口非常适合代表工厂。 在输入上采用Supplier<T>的方法通常应该使用有界的通配符类型( bounded wildcard type)约束工厂的类型参数,以允许客户端传入工厂,创建指定类型的任何子类型。
例如下列代码
public static SpellChecker newSpellChecker(Supplier<? extends Dictionary> supplier) {
return new SpellChecker(supplier.get());
}
客户端调用代码如下
public static void main(String[] args) {
// 假设ChineseDictionary有一个无参构造, 并且不依赖其他资源
Supplier<SpellChecker> supplier = ChineseDictionary::new;
// 使用静态工厂方法创建实例
SpellChecker.newSpellChecker(supplier);
}
总结
尽管依赖注入极大地提高了灵活性和可测试性,但它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。使用依赖注入框架(如Dagger[Dagger]、Guice[Guice]或Spring[Spring])可以消除这些混乱。
总之,不要使用单例或静态的实用类来实现一个类,若类依赖于一个或多个底层资源,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或builder模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。
注:Spring推荐用户使用构造注入
网友评论