C++依赖注入(Dependency Injection DI)vs单例设计模式(Singleton)

C++ 依赖注入(DI)

1、 什么是依赖注入(DI)

1.1 直观定义

依赖注入 = 不在类内部创建依赖对象,而是由外部提供(注入)依赖。

反例(强耦合)

cpp 复制代码
class SlamSystem {
public:
    SlamSystem() {
        imu_ = new ImuDriver();   // 内部创建
        lidar_ = new LidarDriver();
    }
private:
    ImuDriver* imu_;
    LidarDriver* lidar_;
};

依赖注入(低耦合)

cpp 复制代码
class SlamSystem {
public:
    SlamSystem(ImuDriver* imu, LidarDriver* lidar)
        : imu_(imu), lidar_(lidar) {}
private:
    ImuDriver* imu_;
    LidarDriver* lidar_;
};

1.2 为什么需要 DI

问题 不使用 DI 使用 DI
耦合度 极高 低
单元测试 困难 容易
模块替换 需改源码 只换注入对象
架构扩展 痛苦 自然

小结:

DI 是"控制反转(IoC)"的核心实现手段


2、 依赖注入 vs 依赖查找

2.1 依赖查找(Service Locator)

cpp 复制代码
auto imu = ServiceLocator::get<ImuDriver>();

缺点:

  • 隐式依赖
  • 难测试
  • 全局状态

2.2 依赖注入

cpp 复制代码
SlamSystem(ImuDriver& imu);

DI 更显式、更安全、更可维护


3、 C++ 中常见的 4 种依赖注入方式


3.1 构造函数注入(最推荐 ⭐⭐⭐⭐⭐)

适用场景

  • 依赖 必须存在
  • 对象 不可变依赖
cpp 复制代码
class Backend {
public:
    Backend(Optimizer& opt, Map& map)
        : optimizer_(opt), map_(map) {}
private:
    Optimizer& optimizer_;
    Map& map_;
};

优点

  • 强制依赖完整
  • 线程安全
  • 易测试

缺点

  • 构造函数参数可能较多

工程首选


3.2 Setter 注入(可选依赖)

cpp 复制代码
class VisualFrontend {
public:
    void setCamera(Camera* cam) {
        camera_ = cam;
    }
private:
    Camera* camera_ = nullptr;
};

优点

  • 灵活
  • 依赖可后置

缺点

  • 生命周期和空指针风险

适合 可选模块 / 插件


3.3 接口 + 多态注入(经典 OOP)

cpp 复制代码
class Imu {
public:
    virtual ~Imu() = default;
    virtual ImuData read() = 0;
};

class RealImu : public Imu {};
class SimImu  : public Imu {};

class Estimator {
public:
    Estimator(Imu& imu) : imu_(imu) {}
private:
    Imu& imu_;
};

优点

  • 完美支持 mock / fake
  • 解耦实现

SLAM / 机器人中非常常见


3.4 模板注入(编译期 DI,零开销)

cpp 复制代码
template<typename ImuT>
class Estimator {
public:
    Estimator(ImuT& imu) : imu_(imu) {}
private:
    ImuT& imu_;
};

优点

  • 零虚函数开销
  • 编译期绑定

缺点

  • 编译时间增加
  • 接口不稳定

高性能模块(前端、滤波器)推荐


4、 智能指针在 DI 中的使用

4.1 引用 vs 指针 vs 智能指针

类型 场景
T& 必须存在,生命周期外部管理
T* 可为空
std::unique_ptr<T> 所有权转移
std::shared_ptr<T> 共享依赖

推荐原则

cpp 复制代码
class System {
public:
    System(std::shared_ptr<Logger> logger)
        : logger_(std::move(logger)) {}
private:
    std::shared_ptr<Logger> logger_;
};

不要滥用 shared_ptr


5、 手写一个简单 DI Container

cpp 复制代码
class Container {
public:
    template<typename T>
    void registerInstance(std::shared_ptr<T> obj) {
        instances_[typeid(T).hash_code()] = obj;
    }

    template<typename T>
    std::shared_ptr<T> resolve() {
        return std::static_pointer_cast<T>(
            instances_[typeid(T).hash_code()]);
    }

private:
    std::unordered_map<size_t, std::shared_ptr<void>> instances_;
};

使用:

cpp 复制代码
Container c;
c.registerInstance<Imu>(std::make_shared<RealImu>());

auto imu = c.resolve<Imu>();

工程中 不建议自己造复杂轮子


6、 现成 C++ DI 框架

框架 特点
Boost.DI 纯头文件,现代 C++
Google Fruit 谷歌内部风格
Poco DI 工程化
Qt DI Qt 项目

Boost.DI 示例

cpp 复制代码
auto injector = di::make_injector(
    di::bind<Imu>.to<RealImu>()
);

auto estimator = injector.create<Estimator>();

7、 在 SLAM / 机器人系统中的典型 DI 架构

text 复制代码
        Config
          ↓
    Sensor Drivers
          ↓
     Frontend
          ↓
     Backend
          ↓
        Map
cpp 复制代码
SlamSystem(
    Imu& imu,
    Lidar& lidar,
    Camera& cam,
    Backend& backend
);

模块可替换

可单独测试

支持仿真 / 实物


8、 常见反模式(一定要避开)

  • 在类内部 new 依赖
  • 全局单例
  • Service Locator
  • shared_ptr 滥用
  • 构造函数里干重活

9、 总结

依赖注入不是"多写代码",而是"少还技术债"。

核心原则

  • 依赖外置
  • 接口优先
  • 构造函数注入优先
  • 能编译期就编译期
  • 生命周期清晰

建议要点

前端/滤波器 → 模板 DI
系统/模块 → 构造函数 + 接口 DI
测试 → Mock + 注入


依赖注入(DI) vs 单例设计模式(Singleton)

1、核心结论

Singleton 解决的是"唯一性"问题
DI 解决的是"依赖管理与解耦"问题

它们不是同一层面的工具。


2、概念对比

维度 Singleton DI(依赖注入)
关注点 对象唯一性 依赖解耦
控制权 类内部 外部
依赖可见性 ❌ 隐式 ✅ 显式
可测试性 差 好
并发友好 一般 好
架构规模 小~中 中~大
面试评价 会写不加分 会用很加分

3、典型 Singleton 示例

3.1 传统写法(隐式依赖)

cpp 复制代码
class Logger {
public:
    static Logger& instance() {
        static Logger inst;
        return inst;
    }

    void log(const std::string& msg) {
        std::cout << msg << std::endl;
    }
};
cpp 复制代码
void process() {
    Logger::instance().log("processing...");
}

存在问题

  • process() 无法感知依赖
  • 无法 mock
  • 强耦合

4、DI 版本(推荐)

4.1 抽象接口

cpp 复制代码
struct ILogger {
    virtual ~ILogger() = default;
    virtual void log(const std::string&) = 0;
};

4.2 具体实现(可仍是 Singleton)

cpp 复制代码
class ConsoleLogger : public ILogger {
public:
    void log(const std::string& msg) override {
        std::cout << msg << std::endl;
    }
};

4.3 依赖注入(构造注入)

cpp 复制代码
class Processor {
public:
    explicit Processor(ILogger& logger)
        : logger_(logger) {}

    void run() {
        logger_.log("processing...");
    }

private:
    ILogger& logger_;
};

4.4 使用方式

cpp 复制代码
int main() {
    ConsoleLogger logger;
    Processor p(logger);
    p.run();
}

优点

  • 依赖显式
  • 可测试
  • 可替换

5、Singleton + DI(工程中最常见)

Singleton 不消失,但只存在于"组装层"


5.1 组合方式(最佳实践)

cpp 复制代码
class LoggerSingleton : public ILogger {
public:
    static LoggerSingleton& instance() {
        static LoggerSingleton inst;
        return inst;
    }

    void log(const std::string& msg) override {
        std::cout << msg << std::endl;
    }
};
cpp 复制代码
Processor p(LoggerSingleton::instance());

关键点

  • 业务代码不知道这是 Singleton
  • 只依赖接口

6、替代方案 1:Context / Service Locator(折中)

6.1 Context 容器

cpp 复制代码
struct AppContext {
    ILogger* logger;
};
cpp 复制代码
void process(const AppContext& ctx) {
    ctx.logger->log("processing...");
}

特点

优点 缺点
集中管理 仍是全局感
明确依赖 易膨胀

7、替代方案 2:Factory(插件 / 扩展系统)

7.1 Factory + Registry

cpp 复制代码
class LoggerFactory {
public:
    static std::unique_ptr<ILogger> create() {
        return std::make_unique<ConsoleLogger>();
    }
};
cpp 复制代码
auto logger = LoggerFactory::create();
Processor p(*logger);

常用于:

  • 插件系统
  • 动态模块
  • 多策略切换

8、替代方案 3:模板注入(算法/高性能)

8.1 编译期 DI(零开销)

cpp 复制代码
template <typename Logger>
class Processor {
public:
    explicit Processor(Logger& logger) : logger_(logger) {}

    void run() {
        logger_.log("processing...");
    }

private:
    Logger& logger_;
};

特点

  • 零虚函数
  • 编译期绑定
  • 常见于 SLAM / 数值优化

9、真实项目选型建议(非常重要)

用 Singleton 当:

  • Logger
  • 配置只读
  • GPU / CUDA Context
  • 线程池(受控)

用 DI 当:

  • 算法模块
  • 策略选择
  • IO / 存储
  • 可测试代码

不要:

  • 在算法核心用 Singleton
  • 在库代码暴露 Singleton
  • 用 Singleton 传状态

10、二者关系

"Singleton 可以是 DI 的一种实现,
但 DI 不是 Singleton。"


11、对比总结

目标 方案
唯一资源 Singleton
解耦依赖 DI
可测试 DI
高性能 模板 DI
插件系统 Factory
大型系统 DI + 少量 Singleton

相关推荐
轻舟技术A6 小时前
Pi 的 Rust 实现来了:rpi,不止一个终端 Agent
开发语言·后端·rust
王老师青少年编程6 小时前
2026年6月GESP真题及题解(C++三级): 选择题和判断题(题解)
c++·真题·gesp·答案·csp·三级·信奥赛
C++ 老炮儿的技术栈6 小时前
static的作用
c语言·c++·人工智能·单片机·c
额额额对了6 小时前
嵌入式 Linux 启动程序文件
开发语言·嵌入式硬件·php
子非鱼a6 小时前
【WEB】EasySSTI
java·开发语言·前端
奈葵6 小时前
Python
开发语言·python
Nebula_g7 小时前
JavaSE项目实践:红包雨-线程池/线程
java·开发语言
外收内放7 小时前
Python基础语法练习题(56优化版补充)
开发语言·python
小羊没烦恼!7 小时前
jQuery1.5的改进细节
java·服务器·开发语言·前端·c#