选择单例还是依赖注入

在大多数软件架构中,推荐使用依赖注入(即在构造函数或工厂里显式传递服务对象),而不是把服务写成全局的单例。这样做有几个好处:

1. 可测试性和可维护性

  • 构造函数注入(Constructor Injection)让每个命令的依赖关系在类接口上就能一目了然,不会隐藏在某个 Service::instance() 调用里;

  • 单元测试时,只要把一个模拟(Mock)或桩(Stub) 服务对象传进来,就能隔离硬件或底层逻辑,做快速的功能级测试;

  • 如果用单例,你很难在测试里把它替换成假对象,也容易遗留跨测试用例的状态污染。

class MotorController;  // 抽象接口

class StartMotorCommand : public ICommand {
public:
    StartMotorCommand(std::shared_ptr<MotorController> svc)
      : svc_(std::move(svc)) {}
    void execute() override { svc_->start(); }
private:
    std::shared_ptr<MotorController> svc_;
};

2. 生命周期和线程安全

  • 当你显式管理服务的创建和销毁(比如在应用启动时集中 new,一组命令共享同一个 shared_ptr),就能清楚知道它的作用域和销毁时机;

  • 单例往往在首次访问时才创建,也可能依赖静态初始化顺序,容易引发隐藏的初始化竞态(尤其是在多线程环境下);

3. 何时可考虑单例?

尽管单例有上述弊端,但在以下场景下也能适度使用:

  • 全局硬件资源——比如整机只有一套通信总线接口,创建多个实例浪费资源;

  • 第三方库只允许单实例——某些底层 SDK 本身就是单例设计,强制做成全局访问;

  • 非常简单、不可替换的服务——几乎不会有模拟需求,且接口非常稳定、无状态。

如果你确实要用单例,推荐:

  • 给它一个清晰的线程安全接口,使用 C++11 的局部静态变量或 std::call_once 实现;

  • 不要在单例里产生跨模块的全局可变状态;

  • 在单例类上也提供抽象接口(继承于 MotorController),方便将来用 DI 替换或 Mock。

class MotorControllerSingleton : public MotorController {
public:
    static MotorControllerSingleton& instance() {
        static MotorControllerSingleton inst;
        return inst;
    }
    void start() override { /* … */ }
private:
    MotorControllerSingleton() = default;
    // 禁止拷贝/移动
    MotorControllerSingleton(const MotorControllerSingleton&) = delete;
    MotorControllerSingleton& operator=(const MotorControllerSingleton&) = delete;
};

4. 小结

  • 首选依赖注入:传 shared_ptr / unique_ptr / 原始指针,通过构造函数或工厂组装,代码可读、可测、易维护;

  • 仅在实在无法用 DI(或第三方限制)时,才退而求其次选用单例模式,并严格控制可变状态和线程安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值