《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程序员最常用的注入方式。

相关推荐
海边的Kurisu3 小时前
苍穹外卖日记 | Day1 苍穹外卖概述、开发环境搭建、接口文档
java
C雨后彩虹6 小时前
任务最优调度
java·数据结构·算法·华为·面试
heartbeat..6 小时前
Spring AOP 全面详解(通俗易懂 + 核心知识点 + 完整案例)
java·数据库·spring·aop
Jing_jing_X6 小时前
AI分析不同阶层思维 二:Spring 的事务在什么情况下会失效?
java·spring·架构·提升·薪资
元Y亨H8 小时前
Nacos - 服务发现
java·微服务
微露清风8 小时前
系统性学习C++-第十八讲-封装红黑树实现myset与mymap
java·c++·学习
dasi02278 小时前
Java趣闻
java
阿波罗尼亚9 小时前
Tcp SSE Utils
android·java·tcp/ip
susu10830189119 小时前
springboot3.5.8整合minio8.5.9
java·springboot
不知道累,只知道类10 小时前
深入理解 Java 虚拟线程 (Project Loom)
java·开发语言