【问题标题】:C++ and modularity: Where am I supposed to draw the line?C++ 和模块化:我应该在哪里划清界限?
【发布时间】:2011-06-26 17:49:39
【问题描述】:

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

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

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

然后,例如,可以选择广泛使用消息传递系统。但从逻辑上讲,这些又要付出高昂的性能代价。

我应该如何构建这样的工作引擎?

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

例如,具有渲染器类的接口将涉及对时间要求严格的绘图操作的虚函数调用。仅此一项就会使引擎减速相当大。

以下是我的主要问题:

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

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

【问题讨论】:

  • 我建议你将这个问题转发到codereview.stackexchange.com,并附上代码示例来说明问题。
  • 我现在正处于项目的规划阶段,我几乎没有开始编写代码原型——所以我认为这将是一个问题哈哈
  • 嗯,从接口进行虚拟调用的开销(在绝大多数情况下)可以忽略不计。 在分析代码之前,您无法确定。 如果性能影响很大,为什么不简单地消除接口并使用继承或组合呢?
  • 我听说虚拟通话真的很慢,无处不在。所以我想即使没有分析,我也可以肯定它会对渲染性能产生显着的影响,尤其是在移动设备上。关于使用继承和组合,我不知道这是否适合这个问题,因为与使用接口相比,这将不再是真正的模块化。
  • 顺便说一句,我真诚地希望你已经阅读this

标签: c++ class interface game-engine modularity


【解决方案1】:

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

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

此外,模块化是很棘手的,而且不是通过将所有内容都隐藏在界面后面就能得到的。为了使其模块化,无论该接口实现什么都应该可替换。你必须有一个用另一个实现替换一个实现的策略。并且必须可以创建多个不同的实现。

如果你只是盲目地将一切隐藏在接口后面,你的代码将不会是模块化的。替换任何东西的any实现将是一个巨大的痛苦,因为你必须挖掘无数层的接口才能这样做。您必须遍历代码中的数百个地方,并确保选择并实例化并传递正确的实现。而且你的接口太笼统以至于无法表达你需要的功能,或者太具体以至于无法进行其他实现。

如果您想要一个俗气的类比,砖块是模块化的。一块砖可以很容易地取出并换成另一块。但是您也可以将它们研磨成微小的烤粘土颗粒。是不是更模块化?您肯定创建了更多、更小的“模块”。但唯一的影响是让更换任何给定组件变得更加困难。我不能再只是拿起一块有形的砖,扔掉它,然后用其他砖大小的东西代替它。相反,我必须检查数千个小粒子,为每个小粒子找到合适的替代品。而且因为被替换的组件不再被更大结构中的几块砖块包围,而是有数万或数十万个粒子,所以现在有大量其他“模块”受到影响,因为我换掉了它们与之交互的邻居.

将所有东西磨成更细更小的部分并不会使任何东西变得更加模块化。它只是从您的应用程序中删除所有结构。编写模块化软件的方法是真正思考并确定哪些组件在逻辑上如此隔离和独特,以至于可以在不影响应用程序的其余部分的情况下替换它们。然后编写应用程序和组件来保持这种隔离。

【讨论】:

  • 我同意,接口与模块化无关。
  • 在我看来,它们确实很多。使用渲染器接口可以轻松地将默认的 OpenGL 接口与 DirectX 实现互换,而无需重构整个引擎。在我看来,这是非常模块化...
  • @Steve:我没有说他们与模块化没有任何关系。只是它们只是实现它的众多工具之一。 @lamas:您可以在不使用接口的情况下实现这一目标。如果您希望能够在运行时 交换实现,则主要需要接口。您的游戏是否需要能够做到这一点?很可能不会。
  • @jalf:不是我的游戏,但它可能是引擎的一个功能,能够设置启动时使用的 3D 驱动程序。实际上,很多 2D 引擎都提供了这种功能。
  • 如果您只是希望能够在编译时在 OpenGL 和 Direct3D 渲染器之间切换,那么您可以通过放弃接口来实现更高程度的模块化,并且使用模板替代不同的实现。
【解决方案2】:

先做原型,再让界面边界浮现。

抢先式界面设计可以让编码变得拖沓

在编写代码之前尝试设计抽象障碍可能会很棘手,因为您会面临两种风险。一是你不可避免地会在错误的地方画出一些抽象障碍,当你开始编写工作代码(而不是接口代码)时,你会发现你的接口在解决你的问题时效果不佳,尽管用自然语言描述时听起来不错。另一个问题是它使编码变得更加麻烦,因为您必须在脑海中处理两个问题而不是一个问题:为您尚未完全理解的问题编写工作代码,并坚持可能会转向的界面坏了。

接口边界从工作代码中出现。

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

原型,然后只画出你需要的界面边界。

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

【讨论】:

    【解决方案3】:

    我不同意这个建议(或者你的解释)。 “尽可能模块化”:这应该在哪里结束?您是否要为 3d 向量编写虚拟接口,以便可以切换实现?我不这么认为,但它会“尽可能模块化”。

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

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

    【讨论】:

    • 我认为听从您的建议对于大型软件项目来说并不是一个好主意。根据它,我可能会尝试将整个程序塞进 main() 函数中,只是为了不使用任何额外的类。这是个好主意吗?不要这么认为...
    • @lamas:如果它实现了他提出的可靠性、可读性和可维护性的目标,那么为什么不是这是一个好主意?
    • @jalf:那么,这当然是个好主意,但遗憾的是,这在现实中是否可能实现是非常值得怀疑的。如果是,为什么会有这么多关于为什么以及如何将代码重构为许多小函数和多个类的技术和指南?
    • 我不认为我建议你这样做。那将不可读,也不短。
    • @lamas:我认为你没有抓住重点。他就如何实现可靠、可读和可维护的代码提出了一些建议。你说“但如果我把这个发挥到极致,那么一切都会进入主函数”。正如我不太巧妙地指出的那样,如果您可以将所有内容放在主函数中并仍然实现这些目标,那就太好了。更有可能的是,您将无法实现这些目标,因此,“将所有内容都放在 main 中”不是@jdv 建议的逻辑结果。
    【解决方案4】:

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2011-07-18
      • 1970-01-01
      • 2010-09-15
      • 1970-01-01
      • 1970-01-01
      • 2021-01-29
      • 2010-09-30
      • 2011-01-27
      • 2017-06-06
      相关资源
      最近更新 更多