简化 Inversion of Control 和 Dependency Injection
发布: (2025年12月10日 GMT+8 22:00)
5 min read
原文: Dev.to
Source: Dev.to
控制反转(IoC)
控制反转(IoC)是所有现代框架所依赖的基本原则。核心思想很简单:让框架为你完成工作。
当我们使用像 Spring 这样的后端框架时,它会接管大量本来需要手动编写的职责:
- 管理服务器的生命周期
- 处理线程和并发
- 为每个请求进行路由
- 读取和写入 I/O 流
- 序列化和反序列化数据
- 处理异常
- 为所有这些提供测试工具
这就是 IoC:控制权不再在你的代码中,而是交给框架。
应用 IoC 的框架示例
- Django(Python)
- Express.js(Node)
- Gin / Gorilla(Go)
依赖注入(DI)
与 IoC 不同,依赖注入(DI)是一种设计模式。你可以在没有 DI 的框架中使用框架,也可以在没有框架的情况下使用 DI。它的目标是解决一个特定问题:提供依赖。
当一个类需要另一个类实现的功能时,就产生了依赖。例如:
public class EmailSender {
private final EmailWriter emailWriter;
public EmailSender(EmailWriter emailWriter) {
this.emailWriter = emailWriter;
}
public void sendEmail(EmailContent emailContent) {
var email = emailWriter.writeEmail(emailContent);
// etc etc
}
}
在这个例子中 EmailSender 依赖于 EmailWriter;如果不提供该依赖就会导致 NullPointerException。
没有 DI:两种手动解决方案
从外部创建依赖
public class SomeService {
public void notifySomething(Destiny destiny) {
var emailWriter = new EmailSender(new EmailWriter());
EmailContent emailContent = buildEmailContent(destiny);
emailWriter.sendEmail(emailContent);
}
}
在方法内部创建依赖
public class EmailSender {
public void sendEmail(EmailContent emailContent) {
var emailWriter = new EmailWriter();
var email = emailWriter.writeEmail(emailContent);
// etc etc
}
}
这两种做法都有各自的问题:
- 不必要的耦合
- 难以测试
- 无法对依赖进行 Mock
- 创建对象的职责被“移动”到别处
DI 通过把依赖管理交给框架来解决这些问题。
用例:视频 API
假设有一个 API,用户可以上传视频。我们需要:
- 加密
- 发布
- 持久化元数据
此外:
- 支持两种视频格式:MP4 和 FLV
- 开发阶段希望本地保存,生产环境则需要使用云提供商
- 希望能够在不修改服务代码的情况下更换提供商
我们不在代码中写 if (isProd()) { … } else { … } 之类的条件判断,而是使用带有 Profile 的 DI。
公共接口
public interface VideoPublisher {
URI publishContent(VideoPostRequest videoPostRequest);
}
本地实现
@Component
@Profile("local")
public class LocalVideoPublisher implements VideoPublisher {
private static final Logger log = LoggerFactory.getLogger(LocalVideoPublisher.class);
@Override
public URI publishContent(VideoPostRequest videoPostRequest) {
log.info("We save the video locally");
return URI.create("file:///local/test/video123");
}
}
生产实现
@Component
@Profile("prod")
public class CloudVideoPublisher implements VideoPublisher {
private static final Logger log = LoggerFactory.getLogger(CloudVideoPublisher.class);
@Override
public URI publishContent(VideoPostRequest videoPostRequest) {
log.info("Posting to cloud provider!");
return URI.create("https://mycloudprovider.com/uploads/video123");
}
}
使用接口的服务
@Service
public class VideoService {
private final VideoPublisher videoPublisher;
private final VideoRepository videoRepository;
public VideoService(VideoPublisher videoPublisher, VideoRepository videoRepository) {
this.videoPublisher = videoPublisher;
this.videoRepository = videoRepository;
}
public UUID postVideo(VideoRequest request) {
URI link = videoPublisher.publishContent(request.toPostRequest());
// persist metadata with the link …
return videoRepository.save(new Video(link, …));
}
}
带 Profile 的测试
本地环境测试
@SpringBootTest
@ActiveProfiles("local")
class LocalVideoServiceTest {
@Autowired VideoService videoService;
@Autowired VideoRepository videoRepository;
@Test
void shouldSaveLocally() {
var input = new VideoRequest(
VideoType.MP4,
new byte[]{},
"My title",
"My description",
1L
);
var result = videoService.postVideo(input);
var actual = videoRepository.findById(result);
assertThat(actual).get()
.extracting(Video::link)
.isEqualTo(URI.create("file:///local/test/video123"));
}
}
生产环境测试
@SpringBootTest
@ActiveProfiles("prod")
class ProdVideoServiceTest {
@Autowired VideoService videoService;
@Autowired VideoRepository videoRepository;
@Test
void shouldSaveOnCloudProvider() {
var input = new VideoRequest(
VideoType.MP4,
new byte[]{},
"Some other video",
"Significant description",
2L
);
var result = videoService.postVideo(input);
var actual = videoRepository.findById(result);
assertThat(actual).get()
.extracting(Video::link)
.isEqualTo(URI.create("https://mycloudprovider.com/uploads/video123"));
}
}