昨天我问了一个关于单例和模板的问题( Meta programming with singleton ),这引发了一场关于单例使用的争论。我个人不喜欢它们,但对于我的特殊问题,我找不到替代方案。我想描述我的问题,并希望获得关于如何创建可靠解决方案的反馈。
背景:我正在开发的软件已经存在 15 年了,涵盖多个 exe 和 dll(适用于 Windows);我已经为此工作了 6 个月。
我有一个类 Foo,位于共享库中。 Foo 是一个生命周期非常短(~5 秒)的对象,可以在任何线程、任何进程、任何时间创建。我现在用新功能扩展 Foo,要求是必须在执行任何 Foo 对象之前运行名为 FooInit() 的函数,并在进程退出时运行 FooDestroy()。
问题是,Foo 对象的创建是任意的 - 代码的任何部分都可以并且确实调用:
Foo* foo = new Foo();
Foo 的 ctor 中的 boost::call_once 适用于 FooInit(),但不能帮助我解决调用 FooDestroy() 的问题。引用计数 Foo 没有帮助,因为内存中可能随时有 [0,n] 并需要创建更多,所以当计数达到 0 时我无法调用 FooDestroy()。
我当前的解决方案是在 Foo 的 ctor 内部创建并使用“FooManager”单例。单例将调用 FooInit(),并且在将来的某个时间最终将调用 FooDestroy()。我倾向于这个解决方案,因为它似乎是最安全且风险最低的。
如有任何反馈,我们将不胜感激。
最佳答案
如果 FooInit()
和 FooDestroy()
绝对不能多次调用,并且您与一个受虐狂程序员一起工作,他会情不自禁地开枪他或她自己在脚下,那么函数本身需要编写为幂等的,用 FooInit()
注册 FooDestroy()
在程序终止时通过 调用>std::atexit()
.
另一方面,如果 FooInit()
和 FooDestroy()
无法修改,并且您的同事仍然有双脚,那么还有一些替代方案。在深入研究它们之前,让我们简要回顾一下经常针对单例提出的一些论点:
- 这违反了single responsibility principle 。
FooManager
负责通过 RAII 在构造时调用FooInit()
并在销毁时调用FooDestroy()
。将其设为单例后,FooManager
将承担控制其创建和生命周期的额外责任。 - 它们承载着状态。单元测试可能会变得困难,因为一个单元测试可能会影响另一个单元测试。
- 它隐藏了依赖关系,因为单例可以全局访问。
一种解决方案是使用 dependency injection 。通过这种方法,Foo
的构造函数将被修改为接受 FooManager
,并且 FooManager
将:
- 提供一个幂等
init()
方法,该方法仅调用FooInit()
一次。 - 在销毁期间调用
FooDestroy()
。
依赖关系变得明确,FooManager
的生命周期控制何时调用 FooDestroy()
。为了使示例简单,我选择不涉及线程安全,但这是一个基本示例,其中通过使用范围管理 FooManager
的生命周期来重置单元测试之间的状态:
#include <iostream>
#include <boost/noncopyable.hpp>
// Legacy functions.
void FooInit() { std::cout << "FooInit()" << std::endl; }
void FooDestroy() { std::cout << "FooDestroy()" << std::endl; }
/// @brief FooManager is only responsible for invoking FooInit()
/// and FooDestroy().
class FooManager:
private boost::noncopyable
{
public:
FooManager()
: initialized_(false)
{}
void init()
{
if (initialized_) return; // no-op
FooInit();
initialized_ = true;
}
~FooManager()
{
if (initialized_)
FooDestroy();
}
private:
bool initialized_;
};
/// @brief Mockup Foo type.
class Foo
{
public:
explicit Foo(FooManager& manager)
{
manager.init();
std::cout << "Foo()" << std::endl;
}
~Foo()
{
std::cout << "~Foo()" << std::endl;
}
};
int main()
{
// Some unit test that creates Foo objects.
std::cout << "Unit Test 1" << std::endl;
{
FooManager manager;
Foo f1(manager);
Foo f2(manager);
}
// State is not carried between unit test.
// Some other unit test that creates Foo objects.
std::cout << "Unit Test 2" << std::endl;
{
FooManager manager;
Foo f3(manager);
}
}
产生以下输出:
Unit Test 1
FooInit()
Foo()
Foo()
~Foo()
~Foo()
FooDestroy()
Unit Test 2
FooInit()
Foo()
~Foo()
FooDestroy()
如果修改 Foo
的构造并控制 FooManager
的生命周期及其传递方式会产生太大的风险,那么折衷方案可能是通过隐藏依赖关系一个全局性的。然而,为了分割职责并避免携带状态,全局可用的 FooManager 的生命周期可以由其他类型管理,例如智能指针。在下面的代码中:
FooManager
负责在构造时调用FooInit()
并在销毁时调用FooDestroy()
。FooManager
的创建是通过辅助函数进行管理的。FooManager
的生命周期是通过全局智能指针进行管理的。
#include <iostream>
#include <boost/noncopyable.hpp>
#include <boost/scoped_ptr.hpp>
// Legacy functions.
void FooInit() { std::cout << "FooInit()" << std::endl; }
void FooDestroy() { std::cout << "FooDestroy()" << std::endl; }
namespace detail {
/// @brief FooManager is only responsible for invoking FooInit()
/// and FooDestroy().
class FooManager
: private boost::noncopyable
{
public:
FooManager() { FooInit(); }
~FooManager() { FooDestroy(); }
};
/// @brief manager_ is responsible for the life of FooManager.
boost::scoped_ptr<FooManager> manager;
/// @brief Initialize Foo.
void init_foo()
{
if (manager) return; // no-op
manager.reset(new FooManager());
}
/// @brief Reset state, allowing init_foo() to run.
void reset_foo()
{
manager.reset();
}
} // namespace detail
/// @brief Mockup Foo type.
class Foo
{
public:
Foo()
{
detail::init_foo();
std::cout << "Foo()" << std::endl;
}
~Foo()
{
std::cout << "~Foo()" << std::endl;
}
};
int main()
{
// Some unit test that creates Foo objects.
std::cout << "Unit Test 1" << std::endl;
{
Foo f1;
Foo f2;
}
// The previous unit test should not pollute other unit test.
detail::reset_foo();
// Some other unit test that creates Foo objects.
std::cout << "Unit Test 2" << std::endl;
{
Foo f3;
}
}
这会产生与第一个示例相同的输出。
尽管如此,单例或其他解决方案都不能阻止多次调用 FooInit()
,但它们都提供了一种使 FooDestroy()
被调用的方法。程序终止时调用。虽然单例可以为当前问题提供安全且低风险的解决方案,但这是有代价的。单例的后果可能会比其他解决方案产生更多的技术债务,并且可能需要偿还这些债务才能解决 future 的问题。
关于c++ - 任意对象的单例替代方案,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21163490/