简化 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"));
    }
}
Back to Blog

相关文章

阅读更多 »