【设计模式】依赖注入&控制反转

依赖注入 (Dependency Injection, DI) 作为一种设计模式,其核心思想是通用的,可以应用于多种编程语言和环境。

虽然“依赖注入”这个术语在某些框架(如 Spring for Java, ASP.NET Core for C#)中被高度形式化和自动化,但它本质上是一个更广泛的、语言无关的设计原则,用于实现控制反转 (Inversion of Control, IoC)

依赖注入的核心思想 (Language-Agnostic)

  1. 解耦: 一个类(我们称之为“客户端”或“服务消费者”)不应该自己创建它所依赖的其他类(“服务提供者”)的实例。这样会导致硬编码的依赖关系,使得代码难以测试和维护。
  2. 外部注入: 服务提供者的实例应该由外部实体(比如框架、工厂,甚至是另一个类)创建,并“注入”到客户端中。
  3. 面向接口: 客户端依赖于服务的抽象(如接口或抽象类),而不是具体的实现。这样,注入不同的实现就变得非常容易。

控制反转 (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 是最主流和最实用的方式)。

主要优势:

  1. 降低耦合性 (Decoupling): Client 不再依赖于 Service 的具体实现,而是依赖于抽象(如接口)。这使得 Client 与 Service 的具体实现类解耦,更容易替换和升级。
  2. 提高可测试性 (Testability): 在单元测试中,你可以轻松地将一个真实的 Service 替换为一个模拟的 MockService,来隔离地测试 Client 的逻辑。
  3. 提高可维护性 (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 函数会自己编写代码,手动创建 ParserConfigApplication 以及 Application 内部的所有组件(如 PublisherSubscriber)。
  • 这段代码的方式: main 函数只负责解析命令行参数 (parse_cli_options),然后将这个配置 (config) 交给 Application::make_app 这个外部实体(工厂方法) 来创建 Application 对象。main 不再关心 Application 内部是如何被构建的。控制权从 main 反转到了 make_app 方法。

为什么说没有实现“依赖注入”?

依赖注入 (Dependency Injection) 是实现控制反转的一种具体技术手段,它的核心在于 “对象的依赖是由外部提供(注入)给它的,而不是由它自己创建”。

  • 在这段代码中: Application::make_app 方法内部,很可能包含了创建其所有内部组件(如 PublisherSubscriberNetworkTransport 等)的逻辑。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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值