【问题标题】:Eliminating coupling and global state in the "Game" singleton消除“游戏”单例中的耦合和全局状态
【发布时间】:2012-05-20 06:46:42
【问题描述】:

我正在开发一款游戏(编写我自己的物理引擎),并尝试遵循良好的设计来编写它。最近,我遇到了许多难以发现的错误,为了更容易地捕捉这些错误,我一直在尝试编写单元测试。这很难做到,因为我的很多组件都是紧密耦合的,尤其是与game 模块。

在我的game 模块中,我导出了一个单例类实例,它保存了当前的游戏状态、时间、游戏事件等。然而,在阅读了this 并通过研究如何减少这种耦合后,我决定尝试重写该类,使其不再是单例。

这个想法是使用GameState 类,并在各处传递这些对象,以便单元测试可以为测试创建最少的状态。然后大多数函数只是成为游戏状态的函数,并返回一个新的游戏状态。但是,我遇到了一些设计问题

我的实体对象的位置和速度是根据当前时间计算的python属性。这意味着我不能简单地传入 GameState 对象而不将其重写为函数(导致语法错误)。代码示例:

class Entity:
    @property
    def position(self):
        """Interpolate the position from the last known position from that time."""
        # Here, the game class instance is referenced directly, creating coupling.
        dt = game.game_time - self._last_valid_time
        # x_f = x_i + v_i*t + 1/2 at^2
        return self._position + self._velocity * dt + .5 * self._acceleration * dt**2

我对如何解决此问题进行了一些研究,并遇到了Dependency Injection。本质上,我可以将 GameState 'getter' 对象传递给每个实体的初始化程序。所有 GameState 'getter' 对象都会简单地返回当前状态。示例GameStateGetter类:

class GameStateGetter:
    _CurrentState = None

    def update(self, new_state):
        GameStateGetter._CurrentState = new_state

    def __getattr__(self, name):
        # GameState object properties are immutable, so this can't cause issues
        return getattr(GameStateGetter._CurrentState, name)

现在回答我的问题。

  • 使用 GameState 'getter' 对象会是个好主意吗?

一个问题是更新当前游戏状态的接口(我定义了一个update 方法,但这似乎是一个奇怪的接口)。此外,鉴于全局变量的问题是程序状态不可预测,这并不能真正阻止这种情况。

  • 是否有另一种方法可以将game 依赖项“注入”到Entity 类的position 属性?

理想情况下,我想让事情变得简单。 GameStateGetter 类听起来很抽象(即使实现很简单)。如果我的属性可以隐式传递当前状态,那就太好了。

提前感谢您提供的任何帮助!

【问题讨论】:

  • 更常用的术语是“提供者”。 (这可以防止您将提供程序对象与 getter 方法混淆。)是的,使用它们是一个好主意。依赖注入就像暴力:如果它不起作用,你可能没有使用足够的它。
  • @RobertRossney 谢谢!我对依赖注入的新理解现在与我以前见过的一些代码与名为“Provider”的类相关联。也许有了一些额外的信息,这可以被接受为答案!

标签: python unit-testing python-3.x singleton pygame


【解决方案1】:

我不确定第一个问题,但听起来确实有点牵强。

关于第二个问题:单元测试时,可以先导入game模块,然后monkey-patch全局游戏状态为模拟游戏状态。从现在开始,所有功能/属性都将使用模拟游戏状态。我会在所选单元测试框架的 setUp 方法中进行。

编辑:

为什么不在您的游戏 模块中创建模块级属性“GameState”。您可以从任何地方访问它。如果您想要 GameState 的副本,请在其上定义 clone(或 __deepcopy__)方法。

【讨论】:

  • 我想这可以用于单元测试,谢谢!至于我的第二个问题——我实际上是指允许Entity 位置属性访问game 依赖项。我将进行编辑以使其更清晰。
【解决方案2】:

GameStateProvider 注入Entity 构造函数是一个非常简单的起点,随着逻辑变得更加复杂,它会为您留下一些可以重构的东西。

不过,当状态发生变化时,无需使用新状态更新 eachEntity。如果提供者是一个对象,则默认情况下它是可变的。 (为什么你想要一直改变的东西,游戏状态,是不可变的呢?)你可以改变提供者的current_time属性,然后任何时候得到position,它就会包含正确的值。

图案如下所示:

class Entity(object):

   def __init__(self, game_state_provider):
      self.provider = game_state_provider

   @property
   def position(self):
      # lazily evaluate position as a function of the current time
      if self._last_valid_time == self.provider.current_time:
         return self._position
      self._last_valid_time = self.provider.current_time
      self._position = // insert physics here
      return self._position

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-17
    相关资源
    最近更新 更多