2010-04-02 3 views
1

J'écris un jeu qui a beaucoup de composants. Beaucoup d'entre eux dépendent les uns des autres. Lors de leur création, j'arrive souvent dans des situations catch-22 comme "constructeur WorldState nécessite un PathPlanner, mais le constructeur PathPlanner nécessite WorldState."Initialisation de composants avec des interdépendances - anti-modèle possible?

A l'origine, c'était moins un problème, parce que les références à tout ce qui était nécessaire étaient conservées dans GameEngine, et GameEngine a été passé à tout. Mais je n'aimais pas cette impression, car nous avions l'impression que nous donnions trop d'accès à différents composants, ce qui rendait plus difficile l'application des limites.

Voici le code problématique:

/// <summary> 
    /// Constructor to create a new instance of our game. 
    /// </summary> 
    public GameEngine() 
    { 
     graphics = new GraphicsDeviceManager(this); 
     Components.Add(new GamerServicesComponent(this)); 

     //Sets dimensions of the game window 
     graphics.PreferredBackBufferWidth = 800; 
     graphics.PreferredBackBufferHeight = 600; 
     graphics.ApplyChanges(); 

     IsMouseVisible = true; 
     screenManager = new ScreenManager(this); 

     //Adds ScreenManager as a component, making all of its calls done automatically 
     Components.Add(screenManager); 

     // Tell the program to load all files relative to the "Content" directory. 
     Assets = new CachedContentLoader(this, "Content"); 

     inputReader = new UserInputReader(Constants.DEFAULT_KEY_MAPPING); 

     collisionRecorder = new CollisionRecorder(); 

     WorldState = new WorldState(new ReadWriteXML(), Constants.CONFIG_URI, this, contactReporter); 

     worldQueryUtils = new WorldQueryUtils(worldQuery, WorldState.PhysicsWorld); 

     ContactReporter contactReporter = new ContactReporter(collisionRecorder, worldQuery, worldQueryUtils); 

     gameObjectManager = new GameObjectManager(WorldState, assets, inputReader, pathPlanner); 
     worldQuery = new DefaultWorldQueryEngine(collisionRecorder, gameObjectManager.Controllers); 
     gameObjectManager.WorldQueryEngine = worldQuery; 

     pathPlanner = new PathPlanner(this, worldQueryUtils, WorldQuery); 

     gameObjectManager.PathPlanner = pathPlanner; 

     combatEngine = new CombatEngine(worldQuery, new Random()); 
    } 

Voici un extrait de ce qui précède, c'est problématique:

 gameObjectManager = new GameObjectManager(WorldState, assets, inputReader, pathPlanner); 
     worldQuery = new DefaultWorldQueryEngine(collisionRecorder, gameObjectManager.Controllers); 
     gameObjectManager.WorldQueryEngine = worldQuery; 

J'espère que personne ne l'oublie jamais ce paramètre de gameObjectManager.WorldQueryEngine, ou bien il échouer. Voici le problème: gameObjectManager a besoin d'un WorldQuery, et WorldQuery a besoin d'une propriété de gameObjectManager. En dehors de ce problème, il s'agit d'un désordre de maintenabilité, car si les constructeurs ne sont pas appelés dans un ordre spécifique, le programme va planter.

Que puis-je faire à ce sujet? Ai-je trouvé un anti-pattern?

Répondre

5

Les dépendances de classe circulaire ne sont pas toujours une erreur de conception, mais dans ce cas, je dirais que vous demandez des problèmes.

Le problème semble être que vous avez besoin du Controllers à partir de ce GameObjectManager pour initialiser une autre dépendance de GameObjectManager. Mais pourquoi le GameObjectManager doit-il être celui pour créer les contrôleurs? Si plus d'une chose en dépend, alors vous devriez avoir quelque chose comme un ControllerFactory qui vous permet d'initialiser cette dépendance séparément, et de la passer au GameObjectManager via le constructeur.

À en juger par le nom incroyablement générique, GameObjectManager, je dirais que vous avez affaire à un God Object ici. À tout le moins, vous avez Sequential Coupling, et ce n'est pas une bonne chose. Quant à ce que vous pouvez faire à ce sujet, c'est difficile à dire sans savoir exactement ce que fait le GameObjectManager, mais vous devriez essayer de faire ce qui est mentionné ci-dessus - avoir quelque chose d'autre pour créer les "contrôleurs" et les fournir comme dépendance à la GameObjectManager, plutôt que d'avoir cet objet monolithique être le seul responsable de leur création.

+0

Ok, cela fonctionne bien pour l'extrait que j'ai posté, car GameObjectManager n'est pas une dépendance, mais une de ses propriétés. Mais qu'en est-il lorsque deux objets requièrent une instance l'un de l'autre et ne peuvent pas être instanciés sans cela? (Donc, si tout 'GameObjectManager' était requis, et pas seulement' GameObjectManager.Controllers') –

+0

@Rosarch: Ce que vous appelez est appelé une dépendance cyclique. Généralement, ceci est considéré comme une erreur de conception, avec les exceptions notables des structures de données récursives et des objets auxiliaires qui dépendent * seulement * du même objet qui est responsable de leur création et de leur gestion (tel qu'un pool de ressources). Dans d'autres cas, lorsque vous avez une dépendance cyclique, il y a généralement un sous-ensemble de fonctionnalités dans une ou les deux dépendances qui peuvent être refactorisées dans sa propre classe, de sorte qu'au lieu d'avoir deux classes dépendantes l'une de l'autre la troisième (nouvelle) classe. – Aaronaught

+0

Je voudrais également souligner que dans la terminologie de mon jeu, 'GameObject' se réfère à une entité dans le monde, comme un arbre ou le joueur. Cela ne veut pas dire objet au sens de la POO. –

0

Vous pouvez créer un objet de contexte comme suit:

  • Vous définissez une interface usine
  • Vous définissez une clé l'identifie de façon unique chaque composant
  • Pour chacun de vos composants que vous enregistrez une mise en œuvre de la Interface d'usine (qui crée le composant) avec le contexte.
  • Le constructeur de chaque composant recevra un seul argument de type Contexte
  • La première instruction au sein du constructeur sera d'enregistrer le composant avec l'objet de contexte (en utilisant la touche appropriée)
  • Lorsque le constructeur doit initialiser un champ avec un autre composant, il obtiendra ce composant de l'objet de contexte en spécifiant la clé du composant de besoin.
  • Lorsque le contexte reçoit un message d'obtention (clé), il vérifie si un composant est déjà enregistré sous cette clé. Si oui, il le retournera. Sinon, il appellera l'objet usine enregistré sous cette clé.

Ce modèle est largement utilisé dans l'implémentation du compilateur Javac de Sun. Vous pouvez regarder here (code de la classe de contexte) et here (code de l'un des composants constituant le compilateur. Prendre, en particulier, un regard sur le constructeur)

Une autre alternative est que vous passez à une Dependency Injection container qui va résoudre ces dépendances pour vous.

Questions connexes