依赖注入,全称Dependency Injection,简称DI。
在我们深入探讨之前,先来聊聊“依赖”和“注入”这两个比较术语的词。打个比方,你可以把“依赖”想象成电器设备的外接电线,而“注入”就像是把这根电线插到电源插座里,使用依赖注入就好比是你不需要知道电源插座在哪里,只需要知道有人会在你需要的时候为你插上插座,让电流连通,使设备工作。
在软件设计中,“依赖”指的是一个类需要调用的其他类或者服务,而“注入”则是指将这些所需的类或服务传递给需要它们的类的过程。使用“依赖注入”技术,我们可以在运行时将依赖关系传递给对象,而不是让对象自己去创建或查找它们需要的依赖。
如此,则软件中的组件可以在不同环境下重复使用,就像家里的电器可以随意在不同的插座上使用一样,不用担心它们的电源问题。“依赖注入”让我们的代码更加灵活,更容易维护和扩展。
依赖注入其实是控制反转(IOC)的一种实现方式。
控制反转(Inversion of Control,IOC)是一种设计原则,它让我们能够将程序的控制权从程序本身转移到外部容器或框架上。需要控制反转的具体理由主要包括如下几点:
想象一下,如果你是一位导演,你肯定希望能够专注于电影拍摄部分,而不是每天都忙于处理拍摄场地的预订、设备的采购这些琐事。这就是控制反转的魅力所在——它允许我们专注于核心业务逻辑,而将创建对象、管理生命周期等琐事交给框架去处理。
我们的业务需要向用户发送消息。最开始,我们可能使用短信来实现这一功能。随着技术的发展,我们可能改用微信消息,未来甚至可能使用一种全新的通讯方式——比如X信。
如果我们的代码直接依赖于某种特定的消息发送方式,每当需要改变时,我们都需要修改代码,这无疑是非常繁琐和容易出错的。控制反转让我们可以轻松应对这种变化,因为我们只需更换提供服务的对象即可。
除了依赖注入,依赖查找(Dependency Lookup,DL)也是实现控制反转的一种方式。它的思路是在需要的时候,我们主动向一个管理容器询问或查找我们需要的依赖。这种方式比较传统,仍然依赖于容器的API,就像你需要知道每个插座在哪里,以及如何打开开关一样,这对于软件的解耦并不是最佳选择。
通过接口来抽象依赖是一个比较好的编程实践。在这种情况下,组件不会直接创建它们需要的依赖,而是通过引用接口来获得这些依赖的具体实现。这就像是制定一个规则,所有需要电力的设备都必须有一个标准的插头,这样它们就可以从任何一个标准插座获得电力。
注意使用接口不是必需的,你完全可以将一个不实现接口的类型实例注入到程序中。
在这种情况下,组件会提供一个公开的set方法,容器可以通过这个方法将依赖传递给组件。这就好比每个电器都有一个开关,当需要电力时,你只需打开开关即可。
基于构造函数的注入是最直接的方式之一。当创建一个新的对象时,我们可以在构造函数中传递所有需要的依赖。这就像是买一个需要电池的设备时,店家直接给你装好电池,你拿回家就可以直接使用。
在一些现代编程语言中,我们还可以使用注解(Annotations)来实现依赖注入。例如,在Java中,我们可以在构造函数、set方法、私有字段等元素上添加"@Autowired"注解,这样容器就会自动为我们注入依赖。
这就像是有一个自动插电系统,你只需要打开电器,它就会自动为你供电。
注意Spring中不再推荐使用"@Autowired"直接注解字段,因为这样写容易出Bug,还会让程序和依赖注入框架产生耦合。
很多开发语言本身或者通过第三方框架提供了对依赖注入的支持,下边是一些介绍。
在C++标准库中没有对依赖注入的支持,但是社区有Google Fruit、Boost.DI、Hypodermic这样的框架,它们提供了依赖注入的能力。使用这些框架,C++开发者可以更容易地管理对象的生命周期和依赖关系。
Java可能是依赖注入最为流行的领域之一,Spring Framework就是一个典型的例子。它提供了一整套依赖注入的解决方案,极大地简化了Java应用的开发。
在.NET世界中,ASP.NET Core默认就支持依赖注入,另外还可以选择Ninject、Autofac、Spring.NET等框架。这些框架使得.NET开发者可以轻松地实现依赖注入,使他们的应用程序更加模块化和可测试。
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();
}
}
明确你的依赖:在使用依赖注入时,首先要明确你的类需要哪些外部依赖。这就像是在你搭建电路之前,需要知道哪些设备需要电源。
选择合适的注入方式:根据你的具体情况选择最合适的依赖注入方式,无论是setter注入还是构造器注入。这就像是选择合适的充电器插头,确保它与你的设备兼容。
避免过多的依赖:一个类不应该有太多的依赖,这会使得类变得难以管理和维护。想象一下,如果一个电器有太多的插头,使用起来就会变得非常麻烦。
使用依赖注入容器:使用依赖注入容器可以帮助你管理类的依赖关系,这就像是使用一个多功能的电源条,可以同时为多个设备供电。
依赖注入作为一种设计模式,它旨在减少代码之间的耦合性,使代码更易于维护和测试。依赖注入的核心思想是将对象的创建与它们的使用分离开来。通过这种方式,依赖注入可以使代码更加灵活,更容易扩展和重用。随着软件行业的不断发展,依赖注入也在不断进化,它已经从一个编程技巧变成了一种软件设计的基本原则。
关注萤火架构,加速技术提升!