深入理解《Effective Java》 之 条目5:优先考虑通过依赖注入来连接资源
一、为什么静态工具类和Singleton类都不合适连接资源
对于行为会被底层资源以参数化方式影响的类而言,静态工具类和Singleton类都不合适。
1.明确几个概念
(1)什么是“行为被底层资源以参数化方式影响”?
指的是一个类做的事情(行为),不是固定的,而是由某个外部资源决定,而这个资源是通过参数传进来的。
书中例子:一个拼写检查工具依赖词典,每种语言都有自己词典,特殊词汇还要使用特殊词典。这里的 “词典” 就是底层资源,它会影响检查工具的具体行为。
(2)静态工具类(static utility class)
详见条目4,它的特点是无状态、不依赖实例字段、构造器私有,通常被声明为final。
public final class MathUtils {
private MathUtils() {}
public static int add(int a, int b) {
return a + b;
}
}
✅ 适合:真正无状态、行为固定的工具
❌ 不适合:行为随参数变化、依赖外部资源
(3)Singleton(单例)
详见条目3,它的特斯按是全局唯一实例(或线程唯一实例)、生命周期几乎等于JVM
public enum EvlisSingleton{
INSTANC;
public void sing(){
//...
}
}
✅ 适合:天然只有一个实例、状态稳定
❌ 不适合:行为会因参数不同而不同
2.为什么这两者“都不合适”?
核心原因一句话:参数化行为的类,本质上是“多变的、可配置的”,而静态工具类和单例都是“全局固定的”。
(1)静态工具类的问题
参数固定,满足不了参数变化要求。
public class SpellChecker{
private static final Lexicon dictionary=new ChineseLexicon(); // 只有一个中文词典
private SpellChecker(){} // 不可实例化
public static boolean isValid(String word){ // 检测方法
return dictionary.contains(word);
}
}
如果行为被参数化,那么工具类只能强行“假装自己是无状态的”,通过这种方式保存实例化字段,已经不是工具类了。
public class SpellChecker{
private static Lexicon dictionary;
private SpellChecker(){} // 不可实例化
public static void init(Lexicon dictionary){ // 多线程很危险
SpellChecker.dictionary=dictionary;
}
public static boolean isValid(String word){
return dictionary.contains(word);
}
}
(2)Singleton 的问题
参数固定,满足不了参数变化要求。
public class SpellChecker{
private static final Lexicon dictionary=new ChineseLexicon(); // 只有一个中文词典
public static SpellChecker INSTANCE=new SpellChecker(); // Singleton
private SpellChecker(){}
public static boolean isValid(String word){
return dictionary.contains(word);
}
}
Singleton ≠ 参数化实例。Singleton 的本质是:“不管谁、不管什么时候、不管用什么参数,都只能拿到同一个实例。”
但如果行为是由参数决定的:
SpellChecker s1 = new SpellChecker(ChineseLexicon);
SpellChecker s2 = new SpellCheckerr(EnglishLexicon);
这两个对象理应不同,如果你强行用Singleton,要么覆盖已有配置,要么违反单例。
二、为什么推荐使用依赖注入?
静态工具类和 Singleton 适合 “行为固定、全局一致” 的场景;一旦类的“行为要由外部资源参数化决定”,就说明它应该是普通类,通过构造函数 注入依赖。
依赖注入(Dependency Injection,DI)就是:一个对象所需要的“依赖”,不由自己创建,而是由外部“传进来”。 换句话说: “别在自己内部 new,别人给你。” 依赖注入解决了强耦合(自己new耦合度高)、可测试的问题(Mock不能使用)。
(一)依赖注入常见方式
1.构造函数注入(强烈推荐)
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary){
this.dictionary=dictionary;
}
具有依赖不可变(final)、线程安全、对象创建完整优点。
2.Setter 注入(可选依赖)
private Lexicon dictionary;
public void setDictionary(Lexicon dictionary){
this.dictionary=dictionary;
}
依赖是可选的,对象可能处于“半吊子”状态。
3.字段注入(不推荐,常见Spring)
@Autowired
private Lexicon dictionary;
隐藏依赖路径,除非用Spring,否则不建议用。
(二)依赖注入传参形式
1.基础版:资源直接传递
在创建新实例时将资源传入构造器。
词典是拼写检查工具的一个依赖项,在创建该工具的实例时将词典注入其中。
public class SpellChecker{
private final Lexicon dictionary;
private SpellChecker(Lexicon dictionary){
this.dictionary=Object.requireNonNull(dictionary);
}
public static boolean isValid(String word){
return dictionary.contains(word);
}
}
2.升级版:传递资源工厂
(1)什么是资源工厂?
书中说的“资源工厂”,指的是:一个对象,它的唯一职责就是“生产另一种对象”。这通常表现为:
- 一个接口
- 一个
get()/create()/make()方法
Java8引入的Supplier<T>接口就非常适合表示工厂。它的定义:
public interface Supplier<T> {
T get();
}
(2)如何用资源工厂?
我们还以拼写检查工具为例,升级为资源工厂模式。
首先,定义“字典工厂”
// 用 Supplier
Supplier<Dictionary> dictionaryFactory;
// 或自己定义个接口
interface DictionaryFactory {
Dictionary getDictionary();
}
然后,SpellChecker 接收的是“工厂”
class SpellChecker {
private final Supplier<Dictionary> dictionaryFactory;
SpellChecker(Supplier<Dictionary> dictionaryFactory) {
this.dictionaryFactory = dictionaryFactory;
}
public boolean isValid(String word) {
Dictionary dictionary = dictionaryFactory.get();
return dictionary.contains(word);
}
}
最后,测试示例
public static void main(String[] args){
// 每次创建一个新字典
SpellChecker checker =new SpellChecker(() -> new EnglishDictionary());
// 每次返回同一个(相当于单例)
Dictionary dict = new ChineseDictionary();
SpellChecker checker = new SpellChecker(() -> dict);
// 线程级、请求级资源
SpellChecker checker = new SpellChecker(ThreadLocalDictionary::get);
}
SpellChecker 不再持有具体字典,只要知道可以从工厂拿字典用就行,至于什么字典什么时候创建、如何创建,与他无关。
(3)资源工厂有什么优点?
- 延迟创建(
Lazy Creation),字典只有在真正检查时才创建,当然,用SpellChecker不会不创建字典。 SpellChecker的行为由:“你怎么给我字典”决定,而不是:“你给我哪个字典”。- 极高的灵活性,工厂每次new、返回同一个实例、线程级实例等等。
3.高级版:依赖注入框架
向Springboot开发过程中,通过注解@Autowired就是框架依赖注入的一种使用形!大型项目包含成百上千个依赖项,如没有Spring容器管理,那将是灾难性的难度。
总结一条设计原则:如果一个类的行为由外部资源“参数化决定”,那么它就应该是一个普通的、可被实例化的类,而不是全局共享的结构。
更多推荐


所有评论(0)