8

I'm making my first C# app and I am having a bit of difficulty with separation of concerns. I understand the concept, but I don't know if I'm doing it right. I have this as a quick example to illustrate my question. In an app such as a game, there is a main class that runs the main loop, such as Program or Game. My question is, do I maintain every reference to every instance of a class in this class, and make that the only way in which they interact?

For example, a card game with a player, cards, and a board. Let's say the player wants to put cards on the board, but the Player class only has a List<> of Cards and no idea about the game board. However, the main Game class knows about the players, the cards and the gameboard. Should it be up to the Game class to place the cards on the board, or does it make more sense that, because it is the player's action, it should be within the Player class.

Example:

public class Game{
    private GameBoard gameBoard;
    private Player[] players;

   public Game(){
     gameBoard = new GameBoard(10,10);
     Player player1 = new Player();
     Player player2 = new Player();
     players = {player1, player2};
   }

   // Create method here?
   public void PlayerPlaceCard(int x, int y, int cardIndex){
      gameBoard.grid[1,1] = player1.cards[cardIndex];
   }
}

public class Player {
     public List<Cards> cards = new List<Cards>();

     public Player(){
     }

     // Or place method here?
     public PlaceCard(Card card, int x, int y, GameBoard gameBoard){
     }
}

public class GameBoard{
    public Card[,] grid;

    public GameBoard(int x, int y){
       // Make the game board
    }
}

public class Card{
   public string name;
   public string value;
}

To me it makes more sense to have the method in Game, because Game knows about everything. But as I add more code, Game is becoming pretty bloated and I am writing a lot of PlayerDoesThis() functions.

Thanks for any advice

12

The key here is not just separation of concerns, but also the single responsibility principle. The two are basically different sides of the same coin: when I think SOC I think top-down (I have these concerns, how do I separate them?) while SRP is more bottom-up (I have this object, does it have a single concern? Should it be split? Are its concerns split too much already?).

In your example you have the following entities and their responsibilities:

  • Game: this is the code that makes the program "go."
  • GameBoard: maintains the state of the play area.
  • Card: a single entity on the game board.
  • Player: performs actions that change the state of the game board.

Once you think about each entity's single responsibility the lines become more clear.

In an app such as a game, there is a main class that runs the main loop, such as Program or Game. My question is, do I maintain every reference to every instance of a class in this class, and make that the only way in which they interact?

There are really two issues here to keep in mind. The first thing to decide is what entities know about other entities? Which entities belong to other entities?

Look at the responsibilities I outlined above. Players perform actions that change the state of the game board. In other words, Players send messages to (call methods on) the game board. Those messages likely involve cards: for example, a player may place a card in its hand on the board, or change the state of an existing card (e.g. turn a card over or move it to a new location).

Clearly, a player must know about the game board which contradicts the assumption you made in your question. Otherwise, the player must send a message to the game, which then relays that message to the game board. Since players perform actions on the game board, players must know about the game board. This increases coupling: instead of the player sending the message directly, now two actors must know how to send that message. The Law of Demeter implies that if an object must act on another object, in this scenario, that other object should be passed in via parameter to reduce coupling.

Next, where do you store what state? The game is the driver here, it must crease all of the objects either directly or via proxy (e.g. a factory or in a constructor that game calls). The logical next question is what objects need which other objects? This is basically what I asked above, but a different way of asking it.

The way I would design it is like this:

  • Game creates all of the objects required for the game.

  • Game shuffles the cards and divides them up per whatever game it represents (poker, solitaire, etc).

  • Game places the cards in their initial locations: maybe some on the game board, others in players' hands.

  • Game then goes into its main loop representing a turn.

Each turn would look like this:

  • Game sends a message to (invokes a method on) the current player and provides a reference to the game board.

  • Player performs whatever internal logic (computer player) or user interaction necessary to determine what play to perform.

  • Player sends a message to the game board provided to it asking to change the game board state.

  • The game board decides whether the move is valid or not (it is responsible for maintaining valid game state).

  • Control returns to the game, which then decides what to do next. Check for win conditions? Draw? Next player? Next turn? Depends on the specific card game being played.

Should it be up to the Game class to place the cards on the board, or does it make more sense that, because it is the player's action, it should be within the Player class.

Both: Game is responsible for the initial setup, but Player performs actions on the board. GameBoard is responsible for guaranteeing a valid state. For example, in classic Solitaire only the top card on a pile may be face-up.


Back to my original point: you have the right separations of concerns. You identified the proper objects. What tripped you up was figuring out how messages flow through the system and which objects should keep references to other objects. I would design it like this, which is pseudocode:

class Game {
  main();
}

class GameBoard {
  // Data structures specific to the game being played. There is a
  // lot of hand-waving here to give the general idea without
  // getting bogged down in the implementation.
  Map<Card, Location> cards;

  GameBoard(Map<Card, Location>);

  // Return false if the move is invalid.
  bool flip(Card);
  bool move(Card, Location);
}

class Card {
  // Make Rank and Suit enums.
  Suit suit;
  Rank rank;
  bool faceUp;
}

class Player {
  Set<Card> hand;

  Player(Set<Card>);
  void takeTurn(GameBoard);
}
  • 1
    Good answer, just one thing missing I feel. In the OP's example, he's passing an x,y to the gameboard to tell it precisely where to place the card. Whilst the player will call the gameboard to place a card, it should be the gameboard that decides that x,y. Requiring the player to know about board coordinates creates a leaky abstraction. – David Arno Jul 24 '15 at 8:23
  • @DavidArno if the cards can be played at locations in a rectangular grid, how should the player indicate at which of those to play them, if not by coordinates? (Those are not screen coordintes, but grid coordinates.) – Paŭlo Ebermann Jul 24 '15 at 9:39
  • 1
    The specific details about how to place the card should be abstracted somehow by a Location class, which would be a very minor piece in this design. Sure, coordinates might work. It might also be more appropriate to use named locations such as "discard pile." The implementation details are unimportant when mapping out the design as stated in the question. – user22815 Jul 24 '15 at 13:36
  • @Snowman Thanks, this answer is spot on. If we have the Player always acting upon the GameBoard, wouldn't it make more sense to have a local reference within the class, that would be set during its constructor? That way GameBoard wouldn't have to be passed to Player every time (there will be a lot of interactions with the board). – user2410532 Jul 25 '15 at 14:59
  • @DavidArno The player actually needs to tell the gameboard where to place it, the gameboard needs to validate it. The player can pick up and move cards. – user2410532 Jul 25 '15 at 15:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.