运行时切换计算实现的设计模式选型与实践合理性问询
我们有一个通用计算方法Compute,存在多种实现方式,这些实现均继承自IFoo接口,例如FooRed和FooBlue:
class FooRed : public IFoo { public: FooRed() = default; virtual double Compute() override final; // 继承自IFoo };
原本我们通过std::shared_ptr<IFoo>将同一实现实例分发给多个对象,确保它们使用相同的计算方式,初始化方式为:
std::shared_ptr<IFoo> myFoo = std::make_shared<FooRed>();
并在子对象构造时传递:
SubObject(myFoo);
后续开发中,我们需要支持运行时在FooRed与FooBlue之间切换计算模式,为此编写了如下模板类:
template<class T> class Toggleable { protected: std::shared_ptr<T> _instance; bool _state; public: Toggleable(); T* const Get() const { return _instance.get(); } const std::shared_ptr<T>& GetRef() const { return _instance; } virtual void Toggle(); // 将_instance设置为对应类型 };
以及对应的FooManager类:
class FooManager : public Toggleable<IFoo> { public: virtual void Toggle() override; // 继承自Toggleable };
此时UI等模块可通过FooManager的Toggle方法切换模式,子对象通过SubObject(myFoo->GetRef())获取当前实例的引用,确保所有对象使用同一计算方式。
我的初始方案是让FooManager同时实现IFoo接口,类似容器模式:
class FooManager : public Toggleable<IFoo>, public IFoo { public: virtual void Toggle() override; // 继承自Toggleable virtual double Compute() override { return Get()->Compute(); } // 继承自IFoo };
这种方式无需修改原有构造调用,对底层代码改动极小——所有原本接收IFoo的对象无需感知变化,仅处理切换逻辑的模块需要知晓FooManager或Toggleable类型。此外,这种方式让构造函数参数更统一,避免出现SubObject(myFoo->GetRef(), myBar)这种混合共享指针引用与共享指针的尴尬情况。但同事认为这种多重继承属于不良实践,会混淆管理器与实际Foo的职责。
疑问
- 该方案是否为良好实践?
- 如何正确实现这种需要运行时切换多子元素的需求?
- 当前的实现属于何种设计模式,这种运行时选择方式是否合理?
1. 你的多重继承方案算不算良好实践?
不算绝对的“不良实践”,但确实存在职责混淆的风险——FooManager同时承担了**实例管理(切换、持有)和业务计算(Compute)**两个职责。如果后续IFoo扩展更多接口,你需要在FooManager中逐个转发,代码会变得冗余且维护成本上升。
但它的优势也很明显:最小化代码侵入,原有依赖IFoo的代码完全不需要修改,这在大型项目中是很重要的考量。如果你的IFoo接口稳定、不会频繁扩展,这个方案是可接受的折中方案。
2. 正确实现运行时切换多子元素的思路
有几种更清晰的替代方案,可根据场景选择:
方案一:用代理模式替代多重继承
单独实现一个IFoo的代理类,把切换逻辑和代理逻辑分离:
class FooProxy : public IFoo { private: std::shared_ptr<IFoo> _currentFoo; public: void SetCurrentFoo(std::shared_ptr<IFoo> foo) { _currentFoo = foo; } double Compute() override { return _currentFoo->Compute(); } }; // 再单独做一个管理器负责切换 class FooManager { private: std::shared_ptr<FooProxy> _proxy; std::shared_ptr<IFoo> _fooRed; std::shared_ptr<IFoo> _fooBlue; bool _isRed; public: FooManager() : _proxy(std::make_shared<FooProxy>()), _fooRed(std::make_shared<FooRed>()), _fooBlue(std::make_shared<FooBlue>()), _isRed(true) { _proxy->SetCurrentFoo(_fooRed); } void Toggle() { _isRed = !_isRed; _proxy->SetCurrentFoo(_isRed ? _fooRed : _fooBlue); } std::shared_ptr<IFoo> GetFoo() { return _proxy; } };
这种方式职责完全分离:FooProxy只负责转发请求,FooManager只负责切换和持有实例,原有代码依然可以直接接收GetFoo()返回的IFoo指针,同样不需要修改。
方案二:让子对象依赖管理器而非直接依赖IFoo
如果允许修改子对象的构造逻辑,可以让SubObject直接接收FooManager,内部通过GetRef()获取当前实例:
class SubObject { private: std::shared_ptr<FooManager> _manager; public: SubObject(std::shared_ptr<FooManager> manager) : _manager(manager) {} void DoSomething() { double result = _manager->GetRef()->Compute(); // ... } };
这种方式职责最清晰,但需要修改所有子对象的代码,侵入性较高,适合新项目或重构场景。
方案三:改进你的Toggleable模板,让它自带代理能力
把代理逻辑整合到Toggleable模板中,避免多重继承的职责混淆:
template<class T> class ToggleableProxy : public T { protected: std::shared_ptr<T> _instance; bool _state; // 持有所有可选实例 std::shared_ptr<T> _instanceA; std::shared_ptr<T> _instanceB; public: ToggleableProxy(std::shared_ptr<T> a, std::shared_ptr<T> b) : _instanceA(a), _instanceB(b), _state(true) { _instance = _instanceA; } void Toggle() override { _state = !_state; _instance = _state ? _instanceA : _instanceB; } // 转发所有T的接口,这里以Compute为例 double Compute() override { return _instance->Compute(); } }; // 使用方式 auto fooManager = std::make_shared<ToggleableProxy<IFoo>>( std::make_shared<FooRed>(), std::make_shared<FooBlue>() ); // 子对象直接接收fooManager,和原来的用法一致 SubObject(fooManager);
这种方式既保留了原有代码的兼容性,又把代理和管理逻辑整合到一个模板中,职责比多重继承更清晰。
3. 设计模式判断与运行时选择的合理性
你的初始方案本质上是**代理模式(Proxy Pattern)的变体——FooManager作为IFoo的代理,把请求转发给内部的实际实例;同时它还承担了状态管理(切换实例)的职责,也可以看作是策略模式(Strategy Pattern)**的扩展:策略模式负责封装不同算法,而你额外加了一个管理器来动态切换策略实例。
这种运行时切换的方式是完全合理的,尤其是在需要根据用户操作、配置或运行环境动态改变行为的场景下。只要保证切换逻辑线程安全(如果是多线程环境)、实例持有逻辑清晰,就不会有问题。
内容的提问来源于stack exchange,提问作者Root of All Things

