选择单例还是依赖注入
在大多数软件架构中,推荐使用依赖注入(即在构造函数或工厂里显式传递服务对象),而不是把服务写成全局的单例。这样做有几个好处:
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(或第三方限制)时,才退而求其次选用单例模式,并严格控制可变状态和线程安全。