循环类依赖


12

有两个彼此需要的类是不好的设计吗?

我正在写一个小游戏,其中有一个GameEngine包含一些GameState对象的类。要访问几种渲染方法,这些GameState对象还需要了解GameEngine类-因此是循环依赖项。

您会称这个设计不好吗?我只是问,因为我不太确定,目前我仍然可以重构这些东西。


2
如果答案是肯定的,我会感到非常惊讶。
doppelgreener 2012年

由于存在两个问题,因此在逻辑上是不相交的。答案是其中一个。您为什么要设计一个实际上在做某事的状态?您应该有一个经理/控制器来检查状态并执行操作。.让状态类型的对象接管其他任务有点令人困惑。如果您想这样做,C ++是您的朋友
teodron

我的GameState类是诸如介绍,实际游戏,菜单等之类的东西。GameEngine将这些状态存储在堆栈中,因此我能够暂停状态并打开菜单,或播放过场动画,... GameState类需要了解GameEngine,因为引擎创建了窗口并具有渲染上下文。
shad0w 2012年

5
请记住,所有这些规则都是为了快速。快速运行,快速创建,快速维护。有时,这些方面彼此矛盾,您需要进行成本效益分析以决定如何进行。如果您考虑得这么多,并且直言不讳,您仍然比90%的开发人员做得更好。如果您做错了事,没有隐藏的怪物会杀死您,他更像是一个隐藏的收费站运营商。
DampeS8N 2012年

Answers:


0

从本质上来说,这并不是一个糟糕的设计,但它很容易失控。实际上,您正在日常代码中使用该设计。例如,vector知道它是第一个迭代器,并且迭代器具有指向其容器的指针。

现在,根据您的情况,最好为GameEnigne和GameState使用两个单独的类。既然这两个基本上是在做不同的事情,以后您可能会定义很多继承GameState的类(例如游戏中每个场景的GameState)。没有人可以否认他们需要彼此访问。GameEngine基本上是在运行游戏状态,因此应该有一个指向它们的指针。GameState正在使用GameEngine中定义的资源(例如渲染的,物理管理器等)。

您不能也不应将这两个类彼此合并,因为它们天生就在做不同的事情,而合并将导致一个很大的类,没人喜欢。

到目前为止,我们知道在输出设计中需要循环依赖。有多种安全创建方法:

  1. 正如您建议的那样,我们可以将它们每个的指针放到另一个中,这是最简单的解决方案,但很容易失控。
  2. 您还可以使用单例/多例设计,通过这种方法,您应该将GameEngine类定义为单例,并将每个GameState定义为多例。尽管许多开发人员都认为单例和多例都是AntiPattern,但我还是喜欢这种设计。
  3. 您可以使用全局变量,基本上与Singleton / multiton相同,但是它们在事实上略有不同,它们不会限制程序员随意创建实例。

要得出他的答案,您可以使用这三种方法中的任何一种,但是您需要注意不要过度使用它们中的任何一种,因为正如我所说的那样,所有这些设计都非常危险,并且很容易导致代码无法维护。


6

有两个彼此需要的类是不好的设计吗?

这有点代码气味,但是可以保留。如果那是让您的游戏启动并运行的更简便快捷的方法,那就去做吧。但是请记住这一点,因为很有可能您必须在某个时候对其进行重构。

C ++的问题在于,循环依赖项不会那么容易地进行编译,因此摆脱它们而不是花时间修复您的编译可能是一个更好的主意。

有关其他问题,请参见SO上的此问题

您会称我的设计不好吗?

不,这总比将所有课程都上一堂更好。

它不是很好,但是实际上与我见过的大多数实现都非常接近。通常,您会有一个用于游戏状态的管理器类(当心!)和一个渲染器类,并且它们是单例是很常见的。因此,循环依赖是“隐藏的”,但潜在的存在。

另外,正如您在评论中告诉您的那样,游戏状态类执行某种渲染有点奇怪。它们应该只保存状态信息,并且渲染应由渲染器或游戏对象本身的某些图形组件处理。

现在可能会有最终的设计。我很好奇,看看其他答案是否能带来一个好主意。不过,您可能还是可以为您的游戏找到最佳设计的人。


为什么会有代码气味?具有父子关系的大多数对象都需要互相查看。
菊丸

4
因为这是定义哪个类负责什么的最简单方法。它很容易结为两个类,它们之间的耦合如此之深,以至于它们中的任何一个都可以添加新代码,因此它们在概念上不再分开。这也是通向意大利面条代码的门户:类A为此调用了类B,但是类B需要来自类A的信息,依此类推。因此,我不会说子对象应该了解其父对象。如果可能,最好不要这样做。
洛朗·库维杜

那是一个滑溜溜的谬误。静态类也可能导致不好的事情,但是util类没有代码味道。我真的怀疑是否存在某种“良好”的方式来使诸如Scene具有SceneNodes和SceneNode引用Scene之类的事情成为可能,因此您不能将其添加到两个不同的场景中……并且您将在哪里停下来?是A必需的B,B要求C以及C要求A有代码气味吗?
菊丸

确实,这就像静态类。小心处理,仅在必要时使用。现在我是根据经验讲的,我不是唯一这样认为的人,但实际上,这是一个见解的问题。我的意见是,在《任择议定书》所给予的背景下,这确实是难闻的。
洛朗·库维杜

该维基百科文章中没有提到这种情况。在您真正看到过这些班级之前,您不能说这是“不适当的亲密关系”。
菊丸丸2012年

6

通常必须要有两个直接相互引用的类,这是不好的设计。实际上,在代码中跟踪控制流可能会更加困难,对象的所有权及其生存期可能会很复杂,这意味着没有一个类,任何一个类都无法重用,这可能意味着控制流应该真正存在于两个类之外这些类中的第三类是“调解员”类,依此类推。

但是,两个对象相互引用是很常见的,这里的区别是通常一个方向上的关系更加抽象。例如,在Model-View-Controller系统中,View对象可以持有对Model对象的引用,并且将能够了解其所有信息,能够访问其所有方法和属性,以便View可以使用相关数据填充自身。Model对象可以保留对View的引用,以便在其自己的数据发生更改时可以使View更新。但不是模型具有View引用-它将使模型依赖于View-通常,View实现了一个Observable接口,通常只有1个Update()函数,并且模型包含对Observable对象的引用,该对象可以是View。当模型更改时,它将调用Update()其所有Observable,而View将Update()通过回调模型并检索任何更新的信息来实现。这样做的好处是,模型完全不了解视图(为什么要这么做),并且可以在其他情况下重用,即使没有视图也是如此。

您的游戏中也有类似情况。GameEngine通常会知道GameStates。但是GameState不需要了解GameEngine的全部知识,它只需要访问GameEngine上的某些渲染方法即可。这应该在您的脑海中引起一个小警报,该警报是:(a)GameEngine试图在一个类中做太多事情,和/或(b)GameState不需要整个游戏引擎,只需要可渲染部分。

解决此问题的三种方法包括:

  • 使GameEngine从Renderable接口派生,并将该引用传递给GameState。如果您曾经重构过渲染部件,则只需确保它保持相同的接口即可。
  • 将渲染部件分解为新的Renderer对象,然后将其传递给GameStates。
  • 保持原状。毕竟,也许在某个时候您需要从GameState访问所有GameEngine功能。但是请切记,易于维护和易于扩展的软件通常要求每个类在外部尽可能少地引用,这就是为什么将事物分解为执行单个且性能良好的子对象和接口的原因。确定的任务是可取的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.