3
\$\begingroup\$

I've been taking a crack at Domain Driven Design, SOLID principles, and unit tests to write more maintainable code in the future.

Though I tend to run into the same issue: While I have many classes/objects that are easy to test, and do one thing well, I'm completely stumped on how to get classes to interact with each other elegantly.

For example, an Arcade domain, where players spend tokens to play games. The following code will be in TypeScript but I'd imagine this applies to other languages.

  • First I have a class (Value Object) to keep track of Token balances:
    //Value Object: Keeps track of a bag of tokens. 
    class TokenBalance {
      public readonly balance:number;
      constructor(balance: number) {
        if(balance < 0) {
          throw new Error('Token balance must be 0 or higher');
        }
        this.balance = balance;
      }

      AddToBalance(amount: number) {
        return new TokenBalance(this.balance + amount);
      }

      RemoveFromBalance(amount: number) {
        return this.AddToBalance(-amount);
      }
    }
  • Next I have a player (Entity), which has a name, and holds a balance of tokens
    //Value Object: Player Name
    class PlayerName {
      public readonly name: string;
      constructor(name: string) {
        if(!name.length) {
          throw new Error('Name cannot be blank')
        }
        this.name = name;
      }
    }

    //Entity (Aggregate Root?): Holds a name and tokens
    class Player {
      public readonly id: string;
      public readonly name:PlayerName;

      private tokens:TokenBalance;

      constructor(tokens: TokenBalance, name: PlayerName, id:string) {
        this.id = id;
        this.tokens = tokens;
        this.name = name;
      }

      GetTokens() {
        return this.tokens;
      }

      DepositTokens(amount: number) {
        this.tokens = this.tokens.AddToBalance(amount);
      }

      WithdrawTokens(amount: number) {
        this.tokens = this.tokens.RemoveFromBalance(amount);
      }
    }
  • A simple guessing game class (Entity as well?) to keep track of Game State (No logic regarding players at this point)
    enum GameState {
      IN_PROGRESS,
      LOSS,
      WIN
    }

    //Entity?: Maintains invariants of guessing game logic
    class GuessingGame {
      public static MAX_GUESS_COUNT = 5;

      public readonly id:string;
      private correctNumber:number;
      private guesses: number;
      private state: GameState;

      constructor(correctNumber: number, guesses: number, state:GameState, id:string) {
        if(guesses < 0) {
          throw new Error('Negative guesses are impossible');
        }

        this.id = id;
        this.correctNumber = correctNumber;
        this.guesses = guesses;
        this.state = state;
      }

      public GetState() {
        return this.state;
      }

      public Guess(number: number) {
        if(this.state !== GameState.IN_PROGRESS){
          throw new Error('Game is over');
        }
        this.guesses++;
        if(this.guesses >= GuessingGame.MAX_GUESS_COUNT){
          this.state = GameState.LOSS;
        }
        if(this.correctNumber === number) {
          this.state = GameState.WIN;
        }
        return this.state;
      }

      public GetGuessCount() {
        return this.guesses;
      }
    }
  • Finally, the class that I'm most unsure about. The mediator(?) of Games and Players to be used when a player wants to play a game:
    //Domain Service?: Mediates the interactions between a Player and a Game. Ensures that tokens are used before playing a game
    //Unsure about this class. 
    //How can this be retrieved later from a repository when it holds references to 2 entities. How would this be saved to a repository
    class PlayerGuessingGameSession {
      private player: Player;
      private game: GuessingGame;
      private hasStarted: boolean;
      private tokenCost:number;

      constructor(player: Player, game: GuessingGame, tokenCost = 50, hasStarted = false) {
        this.player = player;
        this.game = game;
        this.hasStarted = hasStarted;
        this.tokenCost = tokenCost;
      }

      public StartGame() {
        if(this.hasStarted){
          throw new Error('Game already started');
        }
        this.player.WithdrawTokens(this.tokenCost);
        this.hasStarted = true;
      }

      public Guess(number: number) {
        if(!this.hasStarted) throw new Error('No tokens inserted, cannot start');
        const state = this.game.Guess(number);
        if(state === GameState.WIN){
          this.player.DepositTokens(this.tokenCost + 5); //Reward the user with 5 additional tokens
        }
      }
    }

    //Putting it all together

    const player = new Player(new TokenBalance(100), new PlayerName('Steven'), 'player_id');
    const game = new GuessingGame(5, 0, GameState.IN_PROGRESS, 'some_id');
    const gameSession = new PlayerGuessingGameSession(player, game);

    console.log(player.GetTokens()); // TokenBalance { balance: 100 }
    gameSession.StartGame();
    gameSession.Guess(5);
    console.log(player.GetTokens()); // TokenBalance { balance: 105 }

As the comments may have pointed out, I have a lot of concerns about the last class. If a Player starts a game, leaves, and comes back to continue, I would need a way to retrieve this session from some storage system (repository). Unfortunately, the session contains a Player and a Game entity, so saving a session may cause side effects to the Player and Game entities.

How can I ensure that a game can only be played after tokens are spent, reward players on a win, and give the ability for a player to continue a session later?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

OK. Let's start with some basics, "entities" are just sub-classes - what you really want is aggregate to talk to each other.

In simple terms.

  • AggregateA

    • EntityA1
    • EntityA2
  • AggregateB

    • EntityB1
    • EntityB2

Then...

Only aggregates get to talk / interact with other aggregates. For example... Duncan [my name], is an aggregate of a person, I have entities, like a RightArm entity, a LeftArm entity.

If I got to a Shop, a Shop is an aggregate of many entities.

"So Duncan aggregate interacts with a Shop aggregate."

Side Point Shop has value objects... eg. Number of fork candles for sale. Duncan has value object of... cash to spend.


In your terms, there is two aggregates...

  • Player
  • Game

Player, has tokens. Game has player(s).

Then work out, how they interact, I'd say Game.Start(player1)?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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