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

相关推荐
C^h7 小时前
c++ string
开发语言·c++
raindayinrain8 小时前
c++高性能--原子操作,无锁编程,操作顺序
开发语言·c++·性能优化
学逆向的8 小时前
C++ 合并命名空间
开发语言·c++·网络安全
Zhao-Jichao8 小时前
VS2026 打包 C++ 程序生成 EXE,借助 Installer Project 扩展工具
开发语言·c++
学逆向的9 小时前
C 局部静态变量
c语言·开发语言·网络安全
小徐不徐说9 小时前
C++ 接口与插件设计:从工厂注册到 Qt 动态插件
数据库·c++·qt·系统架构·程序设计
大都督会赢的9 小时前
C++第一节课
开发语言·c++
UIU1149 小时前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
“AI国潮设计-小江”9 小时前
【AIGC实战】Python+Stable Diffusion打造“英歌舞财神”IP,解锁“马到成功”等爆款情绪营销文案设计
开发语言·人工智能·python·prompt·aigc
小王C语言9 小时前
Qt常用控件:容器类控件,标题分组框、标签页(浏览器标签)
开发语言·qt