2012-05-11 54 views
5

Pracuję nad grą (piszę własny silnik fizyki) i staram się postępować zgodnie z dobrym projektem, pisząc ją. Ostatnio natknąłem się na wiele trudnych do znalezienia błędów, a żeby łatwiej je złapać, próbowałem pisać testy jednostkowe. Było to trudne, ponieważ wiele moich komponentów jest ściśle sprzężonych, szczególnie z modułem game.Eliminowanie sprzężenia i stanu globalnego w "Game" singleton

W moim module game eksportuję instancję klasy singleton, która przechowuje bieżący stan gry, czas, zdarzenia związane z grą itp. Jednak po przeczytaniu this i poprzez zbadanie, w jaki sposób zmniejszyć to sprzężenie, postanowiłem spróbować przepisać klasę tak, że nie jest już singletonem.

Chodzi o to, aby użyć klasy GameState i omijać te obiekty wszędzie, tak aby testy jednostkowe mogły stworzyć minimalne stany do testów. Większość funkcji staje się po prostu funkcją stanu gry i zwraca nowy stan gry. Jednakże, Wystąpiły problemy z projektowaniem::

Pozycje i prędkość obiektów obiektu są właściwościami python, które są obliczane zgodnie z bieżącym czasem. Oznacza to, że nie mogę po prostu przekazać obiektu GameState bez przepisywania go jako funkcji (co skutkuje icky składni). Przykładowy kod:

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 

Zrobiłem kilka badań, jak rozwiązać ten problem, i przebiegł Dependency Injection. Zasadniczo mógłbym przekazać obiekt "gettera" GameState do inicjatora każdej jednostki. Wszystkie obiekty "getter" GameState po prostu zwrócą obecny stan. Przykład GameStateGetter klasa:

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) 

Teraz za moje pytania.

  • Czy używanie obiektów GameState "getter" byłoby dobrym pomysłem?

Jednym z problemów będzie interfejs do aktualizacji bieżącego stanu gry (zdefiniowałem metodę update, ale wydaje się to dziwnym interfejsem). Ponadto, ponieważ problem ze zmiennymi globalnymi jest nieprzewidywalnym stanem programu, tak naprawdę nie zapobiegnie to.

  • Czy istnieje inny sposób na „wstrzyknąć” zależność do klasy za Entityposition własności game?

Idealnie, chcę zachować rzeczy proste. Klasa GameStateGetter brzmi bardzo abstrakcyjnie (nawet jeśli implementacja jest prosta). Byłoby miło, gdyby moje właściwości mogły pośrednio przekazywać obecny stan.

Z góry dziękujemy za wszelką pomoc, jaką możesz zapewnić!

+1

Bardziej powszechne jest określenie "operator". (Co powstrzymuje cię od mylenia obiektów dostawcy z metodami getter.) I tak, używanie ich jest dobrym pomysłem. Wstrzykiwanie na uzależnienie jest jak przemoc: jeśli to nie działa, prawdopodobnie nie używasz go w wystarczającym stopniu. –

+0

@RobertRossney Thanks! Moje nowe zrozumienie Dependency Injection łączy się teraz z pewnym kodem, który widziałem wcześniej z klasami o nazwie " Provider". Być może z dodatkowymi informacjami może to zostać zaakceptowane jako odpowiedź! – Darthfett

Odpowiedz

1

Wstrzykiwanie GameStateProvider do konstruktora Entity jest dość prostym miejscem do rozpoczęcia i pozostawia ci coś, co może być refaktoryzowane, gdy twoja logika staje się bardziej złożona.

Nie ma potrzeby aktualizowania każdego z Entity z nowym stanem po zmianie stanu. Jeśli dostawca jest obiektem, domyślnie jest on zmienny. (Dlaczego chcesz, aby coś, co zmienia się cały czas, stan gry, jest tak czy inaczej niezmienne?) Możesz zmienić atrybut current_time u dostawcy, a kiedy tylko coś uzyska position, będzie zawierał poprawną wartość.

Wzór wygląda następująco:

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 
1

Nie mam pewności co do pierwszego pytania, ale brzmi to trochę daleko.

Jeśli chodzi o drugie pytanie: Kiedy przeprowadzasz testowanie jednostkowe, możesz najpierw zaimportować moduł , a następnie zmienić stan gry globalnej na stan fałszywej gry. Odtąd wszystkie funkcje/właściwości będą wykorzystywać stan gry symulacyjnej. Zrobiłbym to w metodzie setUp z wybranej platformy testów jednostkowych.

Edit:

Dlaczego nie uczynić atrybut moduł poziomie "GameState" w gry modułu. Możesz uzyskać do niego dostęp z dowolnego miejsca. Jeśli chcesz mieć kopie GameState, zdefiniuj na nim metodę clone (lub __deepcopy__).

+0

Przypuszczam, że to może zadziałać w przypadku testów jednostkowych, dziękuję! Jeśli chodzi o moje drugie pytanie - tak naprawdę chodziło mi o umożliwienie właściwości pozycji 'Entity', aby uzyskać dostęp do zależności' game'. Będę edytować, aby to wyjaśnić. – Darthfett