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

相关推荐
我在人间贩卖青春2 分钟前
C++之析构函数
c++·析构函数
wenzhangli75 分钟前
ooderA2UI BridgeCode 深度解析:从设计原理到 Trae Solo Skill 实践
java·开发语言·人工智能·开源
灵感菇_19 分钟前
Java 锁机制全面解析
java·开发语言
我在人间贩卖青春20 分钟前
C++之数据类型的扩展
c++·字符串·数据类型
wazmlp00188736931 分钟前
python第三次作业
开发语言·python
娇娇乔木32 分钟前
模块十一--接口/抽象方法/多态--尚硅谷Javase笔记总结
java·开发语言
明月醉窗台44 分钟前
qt使用笔记六之 Qt Creator、Qt Widgets、Qt Quick 详细解析
开发语言·笔记·qt
wangjialelele1 小时前
平衡二叉搜索树:AVL树和红黑树
java·c语言·开发语言·数据结构·c++·算法·深度优先
苏宸啊1 小时前
C++栈和队列
c++
lili-felicity1 小时前
CANN性能调优与实战问题排查:从基础优化到排障工具落地
开发语言·人工智能