《Effective Java》解读第5条:优先考虑依赖注入来引用资源

文章目录

第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) { /* ... */ }
}
  1. 依赖被硬编码:SpellChecker 永远只能使用 EnglishLexicon。如果你想用中文词典或者测试用的专用词典,根本不可能。

  2. 不具备可测试性:你无法在单元测试中为 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);
  1. SpellChecker 是一个通用的拼写检查算法,可以与任何 Lexicon 实现配合使用。
  2. 促进了不变性和线程安全:将依赖声明为 final,并通过构造函数一次性注入,使得类的状态在创建后不可变,这通常更安全、更容易推理。

也可以通过定义Supplier,通过构造器注入一个工厂灵活的进行对象的创建。

虽然依赖注人极大地提升了灵活性和可测试性,但它会导致大型项目凌乱不堪,因为它

通常包含上千个依赖 不过这种凌乱用一个依赖、注入框架或者 Spring ,这也是我们开发最常用的方式。

依赖注入框架

虽然手动进行依赖注入(通过 new)在小型程序中可行,但在大型项目中,管理所有依赖的创建和传递会变得非常繁琐。这时,依赖注入框架(如 Spring, Google Guice, Dagger 等)就派上了用场。

这些框架充当了一个"容器"或"注入器",负责:

  1. 自动创建对象(称为Bean)。

  2. 解析它们之间的依赖关系。

  3. 在运行时将依赖注入到正确的位置。

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 依赖注入的不同方式和优缺点

  1. 构造器注入(官方推荐)
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

易于测试

  1. setter注入
java 复制代码
@Service
public class OrderService {
    private PaymentService paymentService;
    
    // Setter注入
    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

优点:

可选依赖时比较灵活

可以在运行时重新配置

  1. 字段注入(最常用)
java 复制代码
@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private CategoryService categoryService;
}

注意: 虽然写法简单,但不推荐使用

因为:

不利于测试(需要通过反射来注入)

隐藏了依赖关系

不能声明为final

违反了单一职责原则

不过在简洁、清晰、快捷开发的优点下、这些缺点都算事。

这也是java程序员最常用的注入方式。

相关推荐
.柒宇.8 小时前
力扣hot100----15.三数之和(java版)
java·数据结构·算法·leetcode
程序员卷卷狗9 小时前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm
cj6341181509 小时前
【MySQL】mysqldump使用方法
java·后端
JIngJaneIL9 小时前
停车场管理|停车预约管理|基于Springboot的停车场管理系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·停车场管理系统
杰克尼10 小时前
二分查找为什么总是写错
java·数据结构·算法
半旧夜夏12 小时前
【分布式缓存】Redis持久化和集群部署攻略
java·运维·redis·分布式·缓存
短视频矩阵源码定制12 小时前
矩阵系统源码推荐:技术架构与功能完备性深度解析
java·人工智能·矩阵·架构
Eiceblue12 小时前
使用 Java 将 Excel 工作表转换为 CSV 格式
java·intellij-idea·excel·myeclipse
漂流幻境12 小时前
IntelliJ IDEA的Terminal中执行ping命令时遇到的“No route to host“问题
java·ide·intellij-idea