C++和模块化:我应该在哪里划线

C++ and modularity: Where am I supposed to draw the line?

本文关键字:在哪里 我应该 模块化 C++      更新时间:2023-10-16

根据广泛流传的建议,我应该注意让我的大型软件项目尽可能模块化。当然有各种方法可以实现这一点,但我认为没有办法使用或多或少的接口类

以C++中2D游戏引擎的开发为例。

现在,人们当然可以通过使用几乎所有东西的接口来实现一个非常模块化的系统:从渲染器(接口渲染器类->Dummy、OpenGL、DirectX、SDL等),到音频再到输入管理。

然后,例如,可以选择广泛使用消息传递系统。但从逻辑上讲,这些再次带来了高昂的性能代价。

我应该如何建造一个这样的工作引擎

我不想仅仅为了让一个完美的模块化系统在后台工作而降低我的引擎在性能方面的限制(实体、粒子等的最大可行数量)。这很重要,因为我还想针对CPU功率和内存有限的移动平台。

例如,拥有渲染器类的接口将涉及对时间关键的绘图操作的虚拟函数调用。仅此一项就会使发动机减速相当多。

下面是我的主要问题:

  • 模块化编程的一致性和性能之间的界限应该在哪里?

  • 有什么方法可以保持项目模块化,同时为时间关键型操作保持良好的性能?

有很多方法可以在不使用单个"接口"类的情况下保持代码的模块化。

  • 你已经提到消息传递
  • 然后是简单的老回调。如果一个对象需要能够在系统中的其他地方触发某个事件,那么给它一个回调函数,它可以调用该函数来触发该事件。那么它不需要知道任何关于您的架构的其他部分的信息
  • 使用模板和静态多态性,您可以实现与使用接口类相同的大部分目标,但性能开销为零。(例如,为游戏引擎设置模板,以便在编译时可以选择基于Direct3D或OpenGL的渲染器

此外,模块化是很棘手的,而且它不是通过将所有东西隐藏在接口后面就能得到的。为了使其成为模块化的,接口实现的任何东西都应该是可替换的。你必须有一个用另一个实现取代一个实现的策略。并且必须能够创建多个不同的实现

如果你只是盲目地将所有隐藏在接口后面,你的代码将根本不是模块化的。替换任何东西的任何实现都将是一件巨大的痛苦,因为你必须挖掘无数层的接口才能做到这一点。你必须遍历代码中的数百个位置,并确保选择、实例化和传递正确的实现。您的接口过于笼统,以至于无法表达您所需的功能,或者过于具体,以至于无法进行其他实现。

如果你想要一个俗气的类比,砖块是模块化的。一块砖可以很容易地拿出来换成另一块。但你也可以把它们磨成烤粘土的小颗粒。这更模块化了吗?当然,您已经创建了更多更小的"模块"。但唯一的效果是使更换任何给定的组件变得更加困难。我再也不能只拿起一块有形的砖,扔掉,然后换成其他砖大小的东西了。相反,我必须遍历数千个小粒子,为每个粒子找到合适的替代品。由于被替换的组件不再被更大结构中的几块砖包围,而是被数万或数十万个粒子包围,许多其他"模块"现在都受到了影响,因为我换掉了它们的邻居。

把所有东西都磨得越来越细,也不会让任何东西变得更模块化。它只是从应用程序中删除所有结构。编写模块化软件的方法是实际思考,并确定哪些组件在逻辑上是如此孤立和独特,以至于可以在不影响应用程序其余部分的情况下进行替换。然后编写应用程序和组件,以保持这种隔离。

Prototype,然后让接口边界出现

先发制人的接口设计可能会拖累编码

在编写代码之前尝试设计抽象障碍可能很棘手,因为您会面临两个风险。一种是,你不可避免地会在错误的地方设置一些抽象障碍,当你开始编写工作代码(而不是接口代码)时,你会发现你的接口对你的问题的服务很差,尽管用自然语言描述时听起来很好。另一个问题是,它使编码变得更加困难,因为你必须在脑海中处理两个问题,而不是一个:为一个你还不完全理解的问题编写工作代码,以及坚持一个可能会变得糟糕的接口。

接口边界出现在工作代码中

当然,我并不是说接口不好,而是说如果没有先编写工作代码,它们很难正确设计。一旦你有了一个工作程序,很明显,哪些部分应该是同一个虚拟函数的不同实例化,哪些函数需要共享资源(因此应该放在同一个类中),等等

原型,然后只绘制您需要的界面边界

因此,我同意@jdv Jan de Vaan的建议,即首先要做的是推出最短的可读程序。(这与最短的程序不同。当然,即使在刚开始的时候,也会有一些最少量的界面设计。)我补充说,界面设计是在那之后。也就是说,一旦有了尽可能简单的代码,就可以将其重构为接口,使其更短、更可读。如果您想要具有可移植性的接口,在您真正拥有两个或多个平台的代码之前,我不会开始这样做。然后,接口边界将以一种自然的(可测试的)方式出现,因为清楚了哪些函数可以同时使用,哪些函数需要在接口后面隐藏多个实现。

我不同意这个建议(或者你的解释)。"尽可能模块化":这应该在哪里结束?你打算为三维矢量编写一个虚拟接口,这样你就可以切换实现了吗?我不这么认为,但它将"尽可能模块化"。

如果您正在销售游戏引擎,模块化可以帮助降低构建时间,减少潜在客户所需的头文件数量,并能够切换特定问题域的实现(如directx与opengl)。它还可以通过对代码进行分区来帮助使代码可维护。但在这种情况下,不需要将模块与接口解耦。

我的建议是始终编写可读性最短的程序。如果您可以编写20行代码来本地解决某个问题,或者将函数分散在五个不同的类中,后者将更加模块化,但通常结果是不太可靠、可读性较差、可维护性较差。

请记住,虚拟函数调用主要用于处理对象的集合(指向对象的指针/引用),这些对象不一定都是相同的实际类型。

当然,你甚至不应该考虑通过OpenGL绘制正方形,而是通过DirectX绘制圆形,或者任何类似的顺序。在构建代码时,通过模板甚至文件选择来处理这个级别的模块化是完全合理的,但这种情况下的虚拟函数没有实际意义。

这可能带来了从C++中获得性能的相关建议:在保持最大性能的同时,使用灵活性和模块化的模板。模板如此受欢迎的主要原因之一是,它们在不牺牲性能的情况下为您提供模块化功能。CRTP与最初看起来需要虚拟函数的代码特别相关。

至于在一致性和性能之间的界限,实际上没有一个答案——这在很大程度上取决于需要多少性能。对于您的情况(移动设备的3D游戏引擎),性能显然比许多(大多数)其他情况更为关键。