重构单例/全局变量以使用依赖注入进行单元测试

4

我是一个有用的助手,可以为您提供文本翻译。

我正在处理一个使用单例模式和全局变量的大型代码库,现在尝试编写一些单元测试,但是单例模式和全局变量给我带来了很多问题。经过阅读,依赖注入似乎是解决问题的方法。

要进行这种改变的重构任务非常令人望而生畏,我正在努力找出最佳方法。据我理解,基本思路是将类似以下的内容:

foo()
{
    GraphicsCache::Instance()->GetMyImage();
    // do stuff
}

把它转换成类似下面这样的东西:
foo(GraphicsCache *Cache)
{
    Cache->GetMyImage();
    // do stuff
}

这样我就可以制作这些对象的模拟,并在我的测试中使用这些模拟。但是有很多这些类型的对象(事件记录器、网络对象、其他缓存等),这基本上意味着我将不得不在各个地方传递大量对象,并且最终将在各个地方添加大量代码。我理解得对吗?有更好的方法吗?
我想到的一个主意是,有一个单一的对象,它是所有当前全局对象的容器,我只需要传递一个单一的引用,但是这感觉不对,因为大多数地方不需要访问每一个全局对象,只需要子集即可。

关于你关于传递一个单一对象的想法。你只是在传递一个指向全局对象的指针。无论其他程序需要与否并不重要:你并没有传递大量数据,只是一个指针而已。信息确实存在,但他们不必使用它。这就像携带着一个你从未使用过功能的手机一样。它们在那里,但你并不需要使用它们。即使没有这些功能,手机也不会变得更轻。 - cup
2个回答

4
你应该按照你建议的方式去做,传递对象是正确的方法。你可能还想使用工厂模式。是的,你将需要添加很多代码,但这是值得的。
不要创建一个“package”对象来传递,据我所见,它会产生与全局变量完全相同的问题,你无法确定你的对象实际上依赖于什么,因为一切都被传递给它们。
如果你真的没有那么多时间,或者由于其他原因(比如你的经理对此不满意),你也可以创建专门的工厂,并从测试中更改工厂创建的对象。
你可以阅读更多相关内容,这被称为接口隔离原则
简单地说,这个原则指出一个类不应该依赖于它不使用的函数。

这非常有道理,我理解为什么“包”会是个问题。代码库有些混乱,因为单例意味着你可以逃避软件架构问题,事后要解决起来非常困难! - oggmonster
此外,几乎每个类都需要事件记录器,这是否意味着在代码库中的每个类中添加一个事件记录器构造函数参数?这似乎有点疯狂? - oggmonster
@MatthewHubble 我们在工作中也遇到了事件记录器的问题。我们最终做的是在构造函数中创建事件记录器(当然要使用一个工厂,但仍然不是一个好主意),而有些类依赖于全局事件记录器。这个链接可能会有帮助 - https://dev59.com/5nrZa4cB1Zd3GeqP6Lta。对于日志记录问题,我们找不到一个简单的解决方案... - user1708860

1
我从C#的角度回答这个问题,但同样适用于C++。对象绝对比单例更好,但对于真正的横切关注点(即日志记录等),您可能需要考虑将这些单例修改为AmbientContext模式。它基本上是一个强化版的单例,但您可以在应用程序/测试启动时将其实例化为您选择的接口实现。这将使记录器完全可测试,同时不会导致您必须将它们注入到所有内容中。
环境上下文也可以作为临时中间状态,以允许测试,避免一次性重构出所有单例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接