A draughts example app
To illustrate the theory I explained in my earlier blogpost about Code architecture and domain-driven design (please read that one first), let's design an application here to see how it goes. The example we'll use is a website where you can play the board game draughts (sometimes also called checkers) against your friends. I chose this because it's both complicated enough that you need some structure and simple enough to use as an example.
The domain model
We start by designing our domain model. We'll just brainstorm all the classes we think we need for a game of draughts and then group them together in logical aggregates. As a blog isn't the best medium to do that together I've just drawn out the result of my own brainstorm below.
An interesting thing to note is that I put the classes for the game and its state in two
different aggregates. This is because you often need the game without needing its state.
Another interesting thing is that I chose my board class to represent a 'position' (which is
a value object) instead of a 'real board' (which would be an entity). Both are valid
choices, I liked this one.
So now we have an idea of what our domain model is going to be like on paper, let's look at how we would implement it in code.
class GameState : Entity { long Id { get; } private List<Move> _moves; int? CaptureFrom { get; private set; } BoardPosition Board { get; } GameState(long id, List<Move> moves, int? captureFrom) { Id = id; _moves = moves.Clone(); CaptureFrom = captureFrom; Board = BuildBoard(moves); } void AddMove(int from, int to) { // Game logic here... } } |
class BoardPosition : ValueObject { private Piece[] _pieces; Piece this[int squareId] { // Draughts uses 1-based numbers // instead of coordinates get => _pieces[squareId - 1]; } BoardPosition(Piece[] pieces) { // Clone to ensure immutability _pieces = pieces.Clone(); } BoardPosition PerformMove( int from, int to) { // Game logic here... } } |
class Move : ValueObject { int From { get; } int To { get; } Move(int from, int to) { From = from; To = to; } } |
class Piece : ValueObject { Color? PlayerColor { get; } bool IsKing { get; } bool IsEmpty => PlayerColor is null; Piece(Color? color, bool isKing) { PlayerColor = color; IsKing = isKing; } Piece Promoted() { return new Piece(PlayerColor, true); } } |
Note that classes are quite protective. They are responsible to make sure they are always valid, and therefore ensure outsiders can't mess with their fields.
The domain service layer
To make a move on the draughts board, we need logic that involves both the game and its state. But aggregates cannot call other aggregates, and the application layer can't do any (domain) logic at all. So we need a domain service here. It'd look something like this.
class PlayGameDomainService { void DoMove(Game game, GameState gameState, long currentUserId, int from, int to) { game.ValidateCanTakeTurn(currentUserId); gameState.AddMove(from, to); game.NextTurn(currentUserId); } } |
The application context
We now have a domain model that can do all the logic, but we don't actually do anything yet. Let's add a database and a controller, so that we can interact with the real world.
class GameController : Controller { private PlayGameDomainService _playGameService; GameController(PlayGameDomainService playGameService) { _playGameService = playGameService; } [HttpPost("/game/{gameId:long}/move"), Requires(Permissions.PLAY_GAME)] IActionResult DoMove(long gameId, [FromBody] MoveRequest? request) { var game = _gameRepository.FindById(gameId); var gameState = _gameStateRepository.FindById(gameId); _playGameService.DoMove(game, gameState, base.CurrentUserId, request.From, request.To); return Ok(new GameStateDto(gameState)); } record MoveRequest(int? From, int? To); } |
class GameStateRepository { GameState FindById(long gameId) { var dbGameState = SqlQueryBuilder.Init() .SelectAllFrom("gamestates") .Where("game_id").Is(gameId) .Single<DbGameState>(); var dbMoves = SqlQueryBuilder.Init() .SelectAllFrom("moves") .Where("game_id").Is(gameId) .List<DbMove>(); var moves = dbMoves.Select(m => new Move(m.From, m.To)).ToList(); return new GameState(gameId, moves, dbGameState.CaptureFrom); } } |
Wrap up
We've seen some concrete classes to match the ideas from my previous blogpos. We don't have a frontend yet, but since I only wanted to design an API, I'll leave that as an exercise to the reader :).
Related blogposts
- Back to basics - Code architecture; explaining the theory behind the examples in this blogpost.