我是不是对单身人士太过分了?

Am I Going Overkill with Singletons?

本文关键字:是不是 单身      更新时间:2023-10-16

我是c++新手,正在编写跨平台(桌面/手机)2D游戏引擎……我的问题是,我是否以适当的方式使用单例,如果没有,是否有替代方案?

基本上我的引擎有几个组件是围绕一个单例对象构建的。例子:

VBOManager (Singleton)
这个"管理器"基本上负责分配,当然,"管理"用于存储纹理映射和顶点坐标的vbo。我通过这个对象控制"读/写",所以我可以缓存其他对象写入vbo的数据,并返回指针(避免存储重复数据,如500个具有相同映射和顶点坐标的精灵)。

TextureManager (Singleton, self - explanatory)

GLUtils (Singleton)
我经常用这个来统一通用的GL调用这些调用基于当前平台是不同的,比如GL或GLES。例如,GLU函数(libglu不在android上,所以我有一个自定义实现)

图形(Singleton)
用于处理诸如窗口初始化和调整大小,全局帧率管理,视口初始化等。

InputManager (Singleton, self - explanatory)

不管怎样,我在这里回顾了所有这些,我开始感觉真的,真的很脏。我确实觉得考虑到给定对象的需求和功能,这是合理的。但另一方面,我是一个新手,让这种"模式"在我的代码中如此猖獗,感觉有些不对劲。如果确实是后者,那么还有什么替代方案呢?

在我开始之前,我想弄清楚我们正在谈论的内容,以便我们都在同一个页面上。Singleton是:

  1. "一个对象",这意味着它有一定的内部状态,这是从直接外部访问封装。它有一个生命周期:它在程序执行的某个时刻被创建,并在稍后的某个时刻被销毁。
  2. "只能创建一个实例",这意味着有一些显式的机制强制阻止创建多个实例。在程序的执行过程中,构造函数和析构函数只会被调用一次。
  3. "全局可访问。"实例一旦创建,就可以通过全局函数调用或类的公共静态成员来访问。

全局对象不是单例;它只是一个全局对象。如果可以创建多个,则不是单例。

现在定义已经弄清楚了:

VBOManager (Singleton)

我无法想象这个物体是如何工作的。在OpenGL中,缓冲区对象是独立的结构。他们彼此没有关系。所以我不明白为什么你需要一个管理所有的经理。

我可以理解有一个专门的包装缓冲对象类来处理流缓冲对象。有几种方法可以实现流媒体,其中一些比其他的性能更好。所以把它包装在一个对象中是有意义的。

但是拥有一个全局缓冲区对象管理器是没有意义的。缓冲区对象应该彼此独立。或者,如果有的话,它们应该依赖于其他对象,如网格(多个网格对象可以引用相同的缓冲区)。但是你不需要有一个全局缓冲区对象管理器。

我可以理解有一个命名对象的存储库,你可以从中添加和检索。但我不明白为什么需要为这样的特定对象类型使用它。需要它成为全局单例是…可疑的。

我发现单例模式最适合那些根本上唯一的概念。有一个文件系统,因此有一个全局文件系统是有意义的。OpenGL本身是全局的(由于其基于c语言的特性),尽管如此,它仍然可以切换渲染上下文。如果有两个东西是有意义的,那么单例可能不是最好的方法。

即使你只使用一个缓冲区对象,并且有一个管理器,也没有必要把这个类设为单例。如果愿意,您可以将其设置为全局的,但是没有必要强制阻止用户创建该类的多个实例。如果以后,您希望拥有多个缓冲区对象管理器(并且可能要付出性能代价),那么就这样吧。

GLUtils (Singleton)

为什么这是对象?单例模式指的是一个对象在任何时候都只能有一个实例。效用函数只是全局函数

c++不是Java。你不必把所有的东西都放在课堂上。全局函数是不错的设计。事实上,在适当的地方,它们是很好的设计。如果一个函数不需要访问任何状态(除了它的形参和它可能调用的任何其他函数),那么它就没有必要成为对象的一部分。

图形(Singleton)

对于这一点,我们可以认为Singleton是有意义的。它是一个有生命的有状态的物体。而且你明确地不希望在你的应用程序中有超过一个。

但是,考虑一下这个。多生一个有错吗?你传递这个Graphics的频率是多少?有多少代码需要在Graphics对象的级别上工作?我猜不会很多。一些基本的初始化代码,就这些。

因此,虽然它是站得住脚的,但它不是必须的。这不是一个要求成为Singleton的概念。另外,由于能够拥有多个,您可以随时删除并创建一个新的。这使您的应用程序能够更容易地重建窗口。

这样做的一个缺陷是可能同时拥有两个Graphics对象。这涉及到处理OpenGL上下文。由于OpenGL上下文是全局状态,拥有多个Graphics对象可能会导致问题,因为两者都有自己的上下文。你需要一些方法来设置一个特定的Graphics对象作为当前对象,它将自己绑定为当前的OpenGL上下文(并检索它需要的任何函数指针)。

全局变量

单例并不可怕,只是总是有更好的方法。它们的问题在几个人的大型项目中真正开始显现——如果你在一个小的个人项目中使用它们,那么不要觉得你需要为了避免它们而重构你的整个设计。

我听说设计模式"单例"实际上被称为反模式,我认为这是一个有趣的想法。

虽然它可能非常有用,但如果过度使用,它基本上会使您的代码像c一样,因为它使所有内容都可以在任何地方访问(它将您的类变成全局)。如果你在你的单例类中有很多公共方法,你几乎可以从程序的所有部分访问程序的所有操作。

这几乎从来不是好的面向对象设计。虽然它可能在您的头脑中工作,但如果您允许其他人修改您的代码,他们将开始意识到—嘿,当我可以直接调用它时,我为什么要这样做呢?

随着时间的推移,程序可能会退化,变得有点像意大利面条逻辑。

有时,即使只有一个实例,最好将该实例作为普通成员包含在更高级别的实体中。如果您将更高级别的实体设置为单例实体(即使这样我也不一定推荐),那么您几乎可以保证它的成员只有一个实例。谁知道呢?也许有人会找到一个理由,让你选一些你还没有想到的课程。

最重要的是,它在概念上易于遵循,并且您的设计可以很好地封装您的数据。

话虽如此,我在一些地方编程,使全局可访问的东西作为单例似乎值得失去程序的良好的OO设计。在一天结束的时候,这对你来说是一种判断。如果你开始制作太多的单例,那么首先要强烈考虑其他的设计方法,这些方法会有更传统的面向对象的布局——我敢打赌,有9/10次你会发现其中一种其他的设计比单例要好。

我也认为你在滥用singleton。对你列举的例子的建议:

VBOManager

管理vbo只对持有3D网格的类感兴趣。将其转换为Mesh类中的私有或受保护的类变量和成员,以便只有Mesh可以访问此功能。

TextureManager

与vbo相同。这应该只对纹理实例可见,所以它应该在纹理类中作为类成员/变量。

GLUtils

这个我不确定,因为我不清楚它是如何工作的。这需要是一个对象吗?如果这只是一个普通图形函数的集合,那么它能只是一些命名空间下的函数吗?

图形

你说这是窗口、视口等的初始化代码。一旦应用程序初始化,是否需要调用该对象的方法?似乎你的main()函数需要有其中一个设置应用程序运行的适当环境,但是一旦这样做了,你可能不需要使用这个对象,所以也许你可以把它保持在main()的范围内,这样当应用程序退出时,它可以破坏窗口并干净地退出。

InputManager

我不确定你是否需要这个。它是做什么的?你显然需要有Event对象,但通常这些事件是通过某种回调发送给应用程序的,所以应用程序永远不需要与InputManager对象通信。相反,你可以有一个监听事件的后台线程,当一个事件被接收到时,它把它放在一个事件队列中,应用程序读取,或者通过回调函数将它发送给感兴趣的监听器。

我认为单例是一种反模式。让我们回忆一下什么是单例:它是一个只能创建一个实例的类。因此,单例总是全局的。从技术上讲,单例是经过美化的名称空间。如果你将"单例"替换为"全局变量和相关操作函数的命名空间集合",在语义上是相同的。

单例模式几乎没有任何用途。

VBOManager (Singleton)这个"管理器"基本上负责分配,当然,"管理"用于存储纹理映射和顶点坐标的vbo。我通过这个对象控制"读/写",所以我可以缓存其他对象写入vbo的数据,并返回指针(避免存储重复数据,如500个具有相同映射和顶点坐标的精灵)。

vbo绑定到OpenGL上下文,但也可以在OpenGL上下文之间共享。所以有多个VBOManager实例是有意义的,每个OpenGL上下文一个,或者一组共享上下文。

TextureManager (Singleton, self - explanatory)

与VBOs相同。纹理与上下文绑定,但可以共享。

GLUtils (Singleton)我经常用这个来统一通用的GL调用这些调用基于当前平台是不同的,比如GL或GLES。例如,GLU函数(libglu不在android上,所以我有一个自定义实现)

这肯定需要一个命名空间。然而,将功能封装在类中(如GLUTesselator, GLUQuadric等)并允许多个实例不是更有意义吗?

图形(Singleton)

你可以有多个窗口和上下文

InputManager (Singleton, self - explanatory)

输入可能来自多个来源,每个窗口都可能生成它。在网络游戏中,你还需要处理多个输入通道。

游戏似乎只加载了一个世界/地图,或一个输入系统。但如果你能加载多个世界/地图,会有什么害处吗?想想看:能够保持多个地图允许更多的事情,像渲染动态天空盒从另一个地图,加载下一个地图在后台,然后在正确的地方只是交换一个指针。

就我个人而言,我不认为有任何理由要独生子女的。

我的经验是单例要么在任何地方显式声明,要么在业务用例中隐含。就我个人而言,我更喜欢他们,因为当我使用别人的代码时,它会让我很清楚它应该如何工作。

可能会投票-坚持他们-你在做一件好事。

看情况。0)正如其他人所说,如果单例(或者更好地说,全局状态)表示只存在一次的东西,那么它们是有意义的。文件系统,但它真的只有一个吗?)

但是为什么让它成为对象呢?(而不是全局函数/变量)

1)封装——如果有有意义的不变量必须被维护,最好让它只能通过维护它的函数来访问。如果它只是很多自变量,把它变成对象并没有什么帮助,你可以直接访问它们。

2)继承-如果你想使用不同的实现。配置是这样说的,你可以让工厂返回一些子类。你可以不使用对象,但是你可能会重新发明虚函数。

在某些情况下,使用"有效的"单例是更好的——因为你只需要一个,你只使用一个,但如果你开始需要更多,你可以有更多。因此,在代码的"一般"部分,实例应该传递给函数,所以它不会被锁定为默认的一个,而在"业务"代码中,只要你只需要一个,就使用"单例"。