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

相关推荐
weixin_460443568 小时前
2026年新修订《网络安全法》实施:Java私有化考试系统如何做好SBOM、漏洞治理与安全升级?
java·开发语言·在线考试系统·企业培训考试系统·宏远培训考试系统·国企培训考试系统·国产化培训系统
海鸥-w11 小时前
什么是AOP
java
Cc.Y12 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
paopaokaka_luck12 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_9267870512 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev12 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
Sarvartha12 小时前
Lambda 表达式和函数式接口
java·开发语言
计算机毕业设计杰瑞12 小时前
【计算机毕设精品】基于SpringBoot的一体化教务与学业质量管理系统的设计与实现,附源码_增删改查_java项目_可完美运行部署_网站项目
java·spring boot·课程设计
colinu13 小时前
C++ 异步任务与线程生命周期:`std::async`、`future`、`thread`、`jthread` 与协作式退出
java·jvm·c++
蜗牛互联网13 小时前
Python + JSON Schema 实现工单结构化输出与本地复核
java·人工智能·后端