依赖注入 (Dependency Injection, DI) 作为一种设计模式,其核心思想是通用的,可以应用于多种编程语言和环境。
虽然“依赖注入”这个术语在某些框架(如 Spring for Java, ASP.NET Core for C#)中被高度形式化和自动化,但它本质上是一个更广泛的、语言无关的设计原则,用于实现控制反转 (Inversion of Control, IoC)。
依赖注入的核心思想 (Language-Agnostic)
- 解耦: 一个类(我们称之为“客户端”或“服务消费者”)不应该自己创建它所依赖的其他类(“服务提供者”)的实例。这样会导致硬编码的依赖关系,使得代码难以测试和维护。
- 外部注入: 服务提供者的实例应该由外部实体(比如框架、工厂,甚至是另一个类)创建,并“注入”到客户端中。
- 面向接口: 客户端依赖于服务的抽象(如接口或抽象类),而不是具体的实现。这样,注入不同的实现就变得非常容易。
控制反转 (Inversion of Control, IoC)
定义: 将对象创建和管理的控制权从代码本身交给外部容器。
“控制反转”是一种设计原则,它颠覆了传统程序的控制流。在没有 IoC 的传统代码中,一个对象(我们称之为 Client)需要另一个对象(Service)来完成工作,它会主动去创建或查找这个 Service 的实例
// 传统方式:控制权在 Client 手中
class Client {
private:
Service* service_; // Client 自己创建依赖
public:
Client() {
service_ = new Service(); // 硬编码,高耦合
}
void doWork() {
service_->performAction();
}
};
而“控制反转”将这种创建和管理依赖的控制权,从 Client 反转给了一个外部的“第三方”——通常是框架或容器。
// 控制反转:控制权交给外部
class Client {
private:
Service* service_; // 依赖由外部提供
public:
// 依赖由外部注入进来
Client(Service* svc) : service_(svc) {}
void doWork() {
service_->performAction();
}
};
// 在 main 或某个配置类中,由“外部”来决定注入哪个实现
int main() {
Service* realService = new RealService();
Client client(realService); // 控制权在这里,由外部注入
client.doWork();
}
控制反转 VS 依赖注入:
这两个概念常常被混淆,它们的关系如下:
- 控制反转 (IoC) 是目的,是一个宏观的设计思想和原则。
- 依赖注入 (DI) 是实现这个目的的一种具体手段和技术。
你可以通过依赖注入来实现控制反转,但理论上也可能有其他的实现方式(尽管 DI 是最主流和最实用的方式)。
主要优势:
- 降低耦合性 (Decoupling): Client 不再依赖于 Service 的具体实现,而是依赖于抽象(如接口)。这使得 Client 与 Service 的具体实现类解耦,更容易替换和升级。
- 提高可测试性 (Testability): 在单元测试中,你可以轻松地将一个真实的 Service 替换为一个模拟的 MockService,来隔离地测试 Client 的逻辑。
- 提高可维护性 (Maintainability): 由于依赖关系是通过外部配置或注入来管理的,修改依赖关系通常不需要修改 Client 的代码,只需要修改注入的配置即可。
实现方式:
- 构造函数注入 (Constructor Injection): 通过构造函数将依赖传递给对象。这是最常用和推荐的方式。
- Setter 注入 (Setter Injection): 通过对象的 Setter 方法注入依赖。
- 接口注入 (Interface Injection): 通过实现一个特定的接口来注入依赖,这种方式较为少见。
应用场景:
- 框架: Spring (Java), ASP.NET Core (C#) 等现代框架的核心就是基于 IoC 容器,它管理着应用程序中几乎所有对象的生命周期和依赖关系。
- 大型项目: 在复杂的软件系统中,IoC 是管理庞大对象网络的有效方法。
比如fastDDS中的一段代码,其实现的设计模式是IoC,而没有使用依赖注入
auto ret = EXIT_SUCCESS;
CLIParser::benchmark_config config = CLIParser::parse_cli_options(argc, argv);
uint32_t timeout = 1;
switch (config.entity)
{
case CLIParser::EntityKind::PUBLISHER:
timeout = config.pub_config.timeout;
break;
case CLIParser::EntityKind::SUBSCRIBER:
timeout = 0;
break;
default:
break;
}
std::string app_name = CLIParser::parse_entity_kind(config.entity);
std::shared_ptr<Application> app;
try
{
app = Application::make_app(config);
}
catch (const std::runtime_error& e)
{
EPROSIMA_LOG_ERROR(app_name, e.what());
ret = EXIT_FAILURE;
}
为什么说实现了“控制反转”?
控制反转 (Inversion of Control) 的核心在于 “将创建对象的控制权从使用者手中反转给外部”。
- 传统方式: main 函数会自己编写代码,手动创建 Parser、Config、Application 以及 Application 内部的所有组件(如 Publisher, Subscriber)。
- 这段代码的方式: main 函数只负责解析命令行参数 (parse_cli_options),然后将这个配置 (config) 交给 Application::make_app 这个外部实体(工厂方法) 来创建 Application 对象。main 不再关心 Application 内部是如何被构建的。控制权从 main 反转到了 make_app 方法。
为什么说没有实现“依赖注入”?
依赖注入 (Dependency Injection) 是实现控制反转的一种具体技术手段,它的核心在于 “对象的依赖是由外部提供(注入)给它的,而不是由它自己创建”。
- 在这段代码中: Application::make_app 方法内部,很可能包含了创建其所有内部组件(如 Publisher, Subscriber, NetworkTransport 等)的逻辑。Application 类本身并没有暴露一个接口(如构造函数参数或 setter 方法)来让外部将这些依赖“注入”进来。Application 仍然是自己在内部创建和管理它的依赖。
类比
我们可以用一个生活中的例子来类比:
- main 函数: 一个顾客。
- Application::make_app: 一家餐厅的厨师。
- Application: 一道菜。
- 传统方式 (无 IoC): 顾客自己买菜、洗菜、切菜、下厨做菜。
- 这段代码 (IoC, 无 DI): 顾客告诉厨师(make_app)想要什么口味的菜(config),然后由厨师(make_app)完成所有工作,包括采购食材、烹饪。顾客不参与制作过程(控制权反转)。但厨师(make_app)是自己去买菜(创建依赖),而不是由顾客或其他人把菜(依赖)准备好递给厨师(注入)。
- 依赖注入 (DI): 顾客把所有准备好的食材(各种依赖对象)都交给厨师(Application),厨师只负责烹饪(使用这些依赖)。这才是“注入”。
改成依赖注入版本如下
// 假设的依赖注入版本
try
{
// 外部(例如 main 或一个专门的 IoC 容器)负责创建所有依赖
auto publisher = std::make_shared<Publisher>(config.pub_config);
auto subscriber = std::make_shared<Subscriber>(config.sub_config);
// 然后将这些依赖注入到 Application 的构造函数中
app = std::make_shared<Application>(publisher, subscriber, config);
}
catch (const std::runtime_error& e)
{
EPROSIMA_LOG_ERROR(app_name, e.what());
ret = EXIT_FAILURE;
}

889

被折叠的 条评论
为什么被折叠?



