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

相关推荐
Ulyanov2 小时前
Python射击游戏开发实战:从系统架构到高级编程技巧
开发语言·前端·python·系统架构·tkinter·gui开发
Geoking.2 小时前
【设计模式】责任链模式(Chain of Responsibility)详解
java·设计模式·责任链模式
Hello.Reader2 小时前
连接四元组它为什么重要,以及它和端口复用(SO_REUSEPORT)的关系(Go 实战)
开发语言·后端·golang
静待_花开2 小时前
java日期格式化
java·开发语言
我是一只小青蛙8882 小时前
二分查找巧解数组范围问题
java·开发语言·算法
Jayden_Ruan2 小时前
C++水仙花数
开发语言·c++·算法
lsx2024062 小时前
SQL UNIQUE约束详解
开发语言
一只爱做笔记的码农2 小时前
【C#】如何把资源打包成zip压缩包,内嵌进程序中,然后程序可以直接用代码进行访问,无需解压
开发语言·c#
Renhao-Wan2 小时前
数据结构在Java后端开发与架构设计中的实战应用
java·开发语言·数据结构