1 什么是依赖注入
依赖注入,全称Dependency Injection,简称DI。
在我们深入探讨之前,先来聊聊“依赖”和“注入”这两个比较术语的词。打个比方,你可以把“依赖”想象成电器设备的外接电线,而“注入”就像是把这根电线插到电源插座里,使用依赖注入就好比是你不需要知道电源插座在哪里,只需要知道有人会在你需要的时候为你插上插座,让电流连通,使设备工作。
在软件设计中,“依赖”指的是一个类需要调用的其他类或者服务,而“注入”则是指将这些所需的类或服务传递给需要它们的类的过程。使用“依赖注入”技术,我们可以在运行时将依赖关系传递给对象,而不是让对象自己去创建或查找它们需要的依赖。
如此,则软件中的组件可以在不同环境下重复使用,就像家里的电器可以随意在不同的插座上使用一样,不用担心它们的电源问题。“依赖注入”让我们的代码更加灵活,更容易维护和扩展。
2 为什么需要依赖注入
2.1 为什么需要控制反转
依赖注入其实是控制反转(IOC)的一种实现方式。
控制反转(Inversion of Control,IOC)是一种设计原则,它让我们能够将程序的控制权从程序本身转移到外部容器或框架上。需要控制反转的具体理由主要包括如下几点:
- 实际只需要对象提供的服务,不需要关心对象从何而来。
- 时空转变时,可能需要不同的对象来提供类似的服务,比如对于数据库操作服务,单元测试时需要模拟操作,而实际运行时需要真实操作。
- 拥有对象的控制权时,需要不断的改造自己,麻烦且容易出问题。比如依赖项的构造函数变化、具体实现的更改等。
- 将对象创建的控制权交出去,让外部场景来提供合适的服务对象。这可以让程序更容易组件化,更方便组合。
想象一下,如果你是一位导演,你肯定希望能够专注于电影拍摄部分,而不是每天都忙于处理拍摄场地的预订、设备的采购这些琐事。这就是控制反转的魅力所在——它允许我们专注于核心业务逻辑,而将创建对象、管理生命周期等琐事交给框架去处理。
2.2 控制反转的需求举例:发消息
我们的业务需要向用户发送消息。最开始,我们可能使用短信来实现这一功能。随着技术的发展,我们可能改用微信消息,未来甚至可能使用一种全新的通讯方式——比如X信。
如果我们的代码直接依赖于某种特定的消息发送方式,每当需要改变时,我们都需要修改代码,这无疑是非常繁琐和容易出错的。控制反转让我们可以轻松应对这种变化,因为我们只需更换提供服务的对象即可。
2.3 控制反转的其他实现:依赖查找(DL)
除了依赖注入,依赖查找(Dependency Lookup,DL)也是实现控制反转的一种方式。它的思路是在需要的时候,我们主动向一个管理容器询问或查找我们需要的依赖。这种方式比较传统,仍然依赖于容器的API,就像你需要知道每个插座在哪里,以及如何打开开关一样,这对于软件的解耦并不是最佳选择。
3 实现方式
3.1 使用接口
通过接口来抽象依赖是一个比较好的编程实践。在这种情况下,组件不会直接创建它们需要的依赖,而是通过引用接口来获得这些依赖的具体实现。这就像是制定一个规则,所有需要电力的设备都必须有一个标准的插头,这样它们就可以从任何一个标准插座获得电力。
注意使用接口不是必需的,你完全可以将一个不实现接口的类型实例注入到程序中。
3.2 基于set方法
在这种情况下,组件会提供一个公开的set方法,容器可以通过这个方法将依赖传递给组件。这就好比每个电器都有一个开关,当需要电力时,你只需打开开关即可。
3.3 基于构造函数
基于构造函数的注入是最直接的方式之一。当创建一个新的对象时,我们可以在构造函数中传递所有需要的依赖。这就像是买一个需要电池的设备时,店家直接给你装好电池,你拿回家就可以直接使用。
3.4 基于注解
在一些现代编程语言中,我们还可以使用注解(Annotations)来实现依赖注入。例如,在Java中,我们可以在构造函数、set方法、私有字段等元素上添加"@Autowired"注解,这样容器就会自动为我们注入依赖。
这就像是有一个自动插电系统,你只需要打开电器,它就会自动为你供电。
注意Spring中不再推荐使用"@Autowired"直接注解字段,因为这样写容易出Bug,还会让程序和依赖注入框架产生耦合。
4 开发语言支持
很多开发语言本身或者通过第三方框架提供了对依赖注入的支持,下边是一些介绍。
4.1 C++
在C++标准库中没有对依赖注入的支持,但是社区有Google Fruit、Boost.DI、Hypodermic这样的框架,它们提供了依赖注入的能力。使用这些框架,C++开发者可以更容易地管理对象的生命周期和依赖关系。
4.2 Java
Java可能是依赖注入最为流行的领域之一,Spring Framework就是一个典型的例子。它提供了一整套依赖注入的解决方案,极大地简化了Java应用的开发。
4.3 .NET
在.NET世界中,ASP.NET Core默认就支持依赖注入,另外还可以选择Ninject、Autofac、Spring.NET等框架。这些框架使得.NET开发者可以轻松地实现依赖注入,使他们的应用程序更加模块化和可测试。
4.4 PHP
PHP也不例外,Phalcon和Laravel等框架提供了依赖注入的功能,使得PHP开发更加现代化,易于管理和维护。
---
这里为了让大家更好的感受,我们举一个Spring Boot的例子:
首先声明一个日志接口:
public interface ILogger {
void log(String message);
}
然后创建一个实现类,我们使用 @Component 注解来标记 ConsoleLogger 作为一个可注入的组件。
import org.springframework.stereotype.Component;
@Component
public class ConsoleLogger implements ILogger {
@Override
public void log(String message) {
System.out.println("Log: " + message);
}
}
然后创建一个 Application 类,它使用 @Service 注解,并通过构造函数注入 ILogger 依赖。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class Application {
private final ILogger logger;
@Autowired
public Application(ILogger logger) {
this.logger = logger;
}
public void run() {
logger.log("Application is running.");
}
}
在 SpringApplicationMain 主类中注入 Application 类的实例并运行它。SpringApplicationMain 类实现了 CommandLineRunner 接口,当 Spring Boot 应用启动时,run 方法将被自动调用,然后会执行 application.run()。
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SpringApplicationMain implements CommandLineRunner {
private final Application application;
public SpringApplicationMain(Application application) {
this.application = application;
}
public static void main(String[] args) {
SpringApplication.run(SpringApplicationMain.class, args);
}
@Override
public void run(String... args) {
application.run();
}
}
优缺点
5.1 优点
- 代码解耦:通过依赖注入,我们可以降低类与类之间的耦合度。这就好比你的手机、笔记本电脑和台式机可以使用同一个充电器一样方便。这种解耦让每个类更加专注于自己的职责,而不是担心如何与其他类协作。
- 测试便利性:当你需要对某个类进行单元测试时,你可以轻松地替换它依赖的组件,使用模拟(Mock)对象来代替真实的实现。
- 可维护性和可扩展性:随着业务的发展,需求会发生变化。依赖注入使得我们可以不改变现有代码的情况下,扩展或替换组件。这就像是为了应对不同的电压,你可以更换不同的电源适配器,而不是去修改电器本身。
5.2 缺点
- 学习曲线:对于初学者来说,依赖注入可能会带来一定的学习挑战。理解控制反转和依赖注入的概念,以及如何在项目中正确使用它们,可能需要一定的时间和实践。
- 过度设计:在一些简单的应用场景中,使用依赖注入可能会使得项目过度设计,增加不必要的复杂性。这就像是用一个巨大的电源站来给一盏台灯供电,虽然可行,但显然不是最优解。
- 运行时性能:虽然现代依赖注入框架的性能已经非常优秀,但在一些性能敏感的场景下,依赖注入可能会引入轻微的性能开销。这种开销可以比喻为插座和电器之间连接的电线长度,虽然影响微乎其微,但在某些情况下需要考虑。
6 最佳实践
明确你的依赖:在使用依赖注入时,首先要明确你的类需要哪些外部依赖。这就像是在你搭建电路之前,需要知道哪些设备需要电源。
选择合适的注入方式:根据你的具体情况选择最合适的依赖注入方式,无论是setter注入还是构造器注入。这就像是选择合适的充电器插头,确保它与你的设备兼容。
避免过多的依赖:一个类不应该有太多的依赖,这会使得类变得难以管理和维护。想象一下,如果一个电器有太多的插头,使用起来就会变得非常麻烦。
使用依赖注入容器:使用依赖注入容器可以帮助你管理类的依赖关系,这就像是使用一个多功能的电源条,可以同时为多个设备供电。
结语
依赖注入作为一种设计模式,它旨在减少代码之间的耦合性,使代码更易于维护和测试。依赖注入的核心思想是将对象的创建与它们的使用分离开来。通过这种方式,依赖注入可以使代码更加灵活,更容易扩展和重用。随着软件行业的不断发展,依赖注入也在不断进化,它已经从一个编程技巧变成了一种软件设计的基本原则。
关注萤火架构,加速技术提升!