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

相关推荐
代码雕刻家14 小时前
编程语法细节
java·c语言·开发语言
SimonKing14 小时前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
左左右右左右摇晃14 小时前
手写Tomcat原理整理
java·开发语言·笔记·tomcat
孫治AllenSun15 小时前
【JVM】四大引用类型分析
java·开发语言·jvm
于慨15 小时前
安装java
java
夕除15 小时前
sign 是什么
java·前端
Penina101815 小时前
代码如何导入idea
java·intellij idea
.Hypocritical.15 小时前
Maven笔记——2
java·笔记·maven
huohuopro15 小时前
手写 Tomcat 步骤教程
java·网络
snow@li15 小时前
Java:Lombok 完整讲解
java