文章目录
- 第5条:优先考虑依赖注入来引用资源
-
- 静态工具类和单例的不灵活性
- 解决方案:依赖注入
- 依赖注入框架
- 总结
- [spring boot 依赖注入的不同方式和优缺点](#spring boot 依赖注入的不同方式和优缺点)
第5条:优先考虑依赖注入来引用资源
静态工具类和单例的不灵活性
反例1:静态工具类
假设我们有一个拼写检查器,它依赖一本字典。一种看似简单的方法是使用静态工具类。
java
// 不推荐 - 静态工具类不灵活
public class SpellChecker {
private static final Lexicon dictionary = new EnglishLexicon(); // 硬编码依赖
private SpellChecker() {} // 防止实例化
public static boolean isValid(String word) { /* ... */ }
public static List<String> suggestions(String typo) { /* ... */ }
}
-
依赖被硬编码:SpellChecker 永远只能使用 EnglishLexicon。如果你想用中文词典或者测试用的专用词典,根本不可能。
-
不具备可测试性:你无法在单元测试中为 SpellChecker 注入一个模拟的(Mock)字典。
反例2:单例
java
// 不推荐 - 单例同样不灵活
public class SpellChecker {
private final Lexicon dictionary = new EnglishLexicon(); // 同样是硬编码
private SpellChecker() {}
public static SpellChecker INSTANCE = new SpellChecker();
public boolean isValid(String word) { /* ... */ }
public List<String> suggestions(String typo) { /* ... */ }
}
和静态工具类一样,依赖关系在编译期就被固定死了,缺乏灵活性。
这两种模式的共同缺陷在于:它们都假设程序中只存在一种有用的依赖实现。在真实的、复杂的应用中,这是非常不现实的。
解决方案:依赖注入
依赖注入模式解决了上述所有问题。其核心思想非常简单:不要在类的内部创建其依赖的资源,而是将这些资源(依赖)通过构造函数、工厂方法或设置器(Setter)从外部"注入"进去。
java
// 推荐 - 使用依赖注入
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) { /* ... */ }
}
优势:灵活性
java
// 可以轻松使用不同的依赖
Lexicon englishLexicon = new EnglishLexicon();
Lexicon scienceLexicon = new ScienceLexicon();
Lexicon mockLexicon = new MockLexicon(); // 用于测试
SpellChecker englishChecker = new SpellChecker(englishLexicon);
SpellChecker scienceChecker = new SpellChecker(scienceLexicon);
SpellChecker testChecker = new SpellChecker(mockLexicon);
- SpellChecker 是一个通用的拼写检查算法,可以与任何 Lexicon 实现配合使用。
- 促进了不变性和线程安全:将依赖声明为 final,并通过构造函数一次性注入,使得类的状态在创建后不可变,这通常更安全、更容易推理。
也可以通过定义Supplier,通过构造器注入一个工厂灵活的进行对象的创建。
虽然依赖注人极大地提升了灵活性和可测试性,但它会导致大型项目凌乱不堪,因为它
通常包含上千个依赖 不过这种凌乱用一个依赖、注入框架或者 Spring ,这也是我们开发最常用的方式。
依赖注入框架
虽然手动进行依赖注入(通过 new)在小型程序中可行,但在大型项目中,管理所有依赖的创建和传递会变得非常繁琐。这时,依赖注入框架(如 Spring, Google Guice, Dagger 等)就派上了用场。
这些框架充当了一个"容器"或"注入器",负责:
-
自动创建对象(称为Bean)。
-
解析它们之间的依赖关系。
-
在运行时将依赖注入到正确的位置。
java
// 在一个Spring Boot应用中
@Component
public class EnglishLexicon implements Lexicon { /* ... */ }
@Component
public class SpellChecker {
private final Lexicon dictionary;
// Spring会自动从容器中找到一个Lexicon类型的Bean(比如EnglishLexicon)注入到这里
@Autowired
public SpellChecker(Lexicon dictionary) {
this.dictionary = dictionary;
}
}
总结
"优先考虑依赖注入来引用资源"的精髓在于:
核心原则:不要由类内部主动创建其依赖(控制反转)。
实现方式:通过构造函数、设置器或接口将依赖从外部传入。
带来的好处:
解耦:将使用资源的类与资源的创建、配置逻辑分离。
灵活:客户端可以轻松地替换不同的实现。
可测试:这是实现高质量单元测试的基石。
进阶应用:当需要多个实例时,注入工厂(如 Supplier)。
实践工具:对于大型项目,强烈推荐使用成熟的依赖注入框架(如Spring)来管理复杂的依赖关系。
遵循这一条,是编写松耦合、高内聚、易于测试和维护的现代化Java代码的关键一步。
spring boot 依赖注入的不同方式和优缺点
- 构造器注入(官方推荐)
java
@Service
public class UserService {
private final UserRepository userRepository;
// 构造器注入
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 或者(Spring 4.3+ 可以省略 @Autowired)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
优点:
依赖不可变(final字段)
完全初始化的状态
保证依赖不为null
易于测试
- setter注入
java
@Service
public class OrderService {
private PaymentService paymentService;
// Setter注入
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
优点:
可选依赖时比较灵活
可以在运行时重新配置
- 字段注入(最常用)
java
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private CategoryService categoryService;
}
注意: 虽然写法简单,但不推荐使用
因为:
不利于测试(需要通过反射来注入)
隐藏了依赖关系
不能声明为final
违反了单一职责原则
不过在简洁、清晰、快捷开发的优点下、这些缺点都算事。
这也是java程序员最常用的注入方式。