I'm building cross-platform application (every mobile/desktop OS + web in ASP.NET). I'm using Xamarin, so I can do everything in C#. But before I start working on UIs, I want to build soild, flexible Core/Domain project that will be shared in all Client apps.
I read a lot about DI and IoC containers, I know a lot of theory, but when it comes to coding I'm confused and sometimes I've a feeling I have no idea what am I doing.
Quickly about my app: It will be simple game, like tic tac toe. But I want to do it as flexible as possible, so it will be easy to build on top of it another games, like: connect four or reversi or even chess. Simple 2 players board games.
My concerns are about my "DI-friendly" implementation and the way of using IoC container. My project has 3 main layers: UI<-API<-Domain(Entites). In UI I need to create IGame
object. Game has its GameType
(just an enum), List<IPlayers>
and IBoard
.
My question(s) are:
Should I resolve my interfaces in one place? Good example is
IBoard
in my project. It is needed and used only deep inGame
object.Game
obviously depends onIBoard
. So as I read it'd be good practice to injectIBoard
through constoructor - so it's clear: "Game depends on Board". But this way, I createBoard
instance in UI, then I have to pass it toGameAPI
, then toGameFactory
and finally toGame
object. I could easily create it or resolve it inGame
constructor, without passing through every layer in application.Where exactly should I resolve my interfaces? Should it be on very top in UI? Should it be in one place? Where should I create/resolve my
IBoard
?
I did't want to paste here fragments of code and force you to paste it back to IDE, so I've made "Short, Self Contained, Correct (Compilable), Example" and put it here.
Here's my UI:
namespace Tictactoe.UI
{
public partial class Form1 : Form
{
private GameAPI GameAPI { get; } = new GameAPI();
private IGame CurrentGame { get; set; }
IKernel kernel = new StandardKernel();
public Form1()
{
InitializeComponent();
// Load all dependencies
var modules = new List<INinjectModule> { new TictactoeModule() };
kernel.Load(modules);
}
private void buttonNewGameSingle_Click(object sender, EventArgs e)
{
StartNewGame(GameType.SinglePlayer);
}
private void buttonNewTwoPlayers_Click(object sender, EventArgs e)
{
StartNewGame(GameType.TwoPlayers);
}
private void buttonNewGameOnline_Click(object sender, EventArgs e)
{
StartNewGame(GameType.Online);
}
private void StartNewGame(GameType type)
{
EnableBoard(true);
ResetBoard();
// Without IoC
//IPlayerFactory playerFactory = new PlayerFactory(); // Resolve player facotry
//IGameFactory gameFactory = new GameFactory(playerFactory); // Resolve game facotry
//IBoard board = new Board(3,3); // Resolve board
// With IoC
IPlayerFactory playerFactory = kernel.Get<IPlayerFactory>();
IGameFactory gameFactory = kernel.Get<IGameFactory>(new ConstructorArgument("playerFactory", playerFactory));
IBoard board = kernel.Get<IBoard>(new ConstructorArgument("width", 3), new ConstructorArgument("height", 3));
CurrentGame = GameAPI.CreateGame(gameFactory, type, board);
}
private void button_Click(object sender, EventArgs e)
{
Button clicked = sender as Button;
if (clicked != null)
{
int[] xy;
buttons.TryGetValue(clicked.Name, out xy);
Console.WriteLine(clicked.Name + "[" + xy[0] + "," + xy[1] + "]");
if (CurrentGame.IsMoveValid(xy[0], xy[1]))
{
IMove result = CurrentGame.MakeMove(xy[0], xy[1]);
clicked.Text = result.PlayerId.ToString();
if (result.IsConnected)
{
EnableBoard(false);
MessageBox.Show("Finished! Player " + result.PlayerId + " won!");
}
else if (result.IsGameOver)
{
EnableBoard(false);
MessageBox.Show("Game over!!");
}
}
}
}
#region UI Helpers
private Dictionary<string, int[]> buttons = new Dictionary<string, int[]>
{
{ "button1", new[] {0, 0} },
{ "button2", new[] {0, 1} },
{ "button3", new[] {0, 2} },
{ "button4", new[] {1, 0} },
{ "button5", new[] {1, 1} },
{ "button6", new[] {1, 2} },
{ "button7", new[] {2, 0} },
{ "button8", new[] {2, 1} },
{ "button9", new[] {2, 2} }
};
private void EnableBoard(bool enable)
{
button1.Enabled = enable;
button2.Enabled = enable;
button3.Enabled = enable;
button4.Enabled = enable;
button5.Enabled = enable;
button6.Enabled = enable;
button7.Enabled = enable;
button8.Enabled = enable;
button9.Enabled = enable;
}
private void ResetBoard()
{
button1.Text = string.Empty;
button2.Text = string.Empty;
button3.Text = string.Empty;
button4.Text = string.Empty;
button5.Text = string.Empty;
button6.Text = string.Empty;
button7.Text = string.Empty;
button8.Text = string.Empty;
button9.Text = string.Empty;
}
#endregion
}
}
Here's my DependencyResolver
:
namespace Tictactoe.DependencyResolver
{
public class TictactoeModule : NinjectModule
{
public override void Load()
{
Bind<IPlayerFactory>().To<PlayerFactory>();
Bind<IGameFactory>().To<GameFactory>();
Bind<IBoard>().To<Board>();
}
}
}
Here's my API. UI uses only API methods.
namespace TicTactoe.API
{
public class GameAPI
{
public IGame CurrentGame { get; set; }
public IGame CreateGame(IGameFactory gameFactory, GameType type, IBoard board)
{
CurrentGame = gameFactory.CreateGame(type, board);
return CurrentGame;
}
public IMove MakeMove(int row, int column)
{
if (CurrentGame == null) return null;
return CurrentGame.MakeMove(row, column);
}
//TODO: Online player move, or bot
}
}
And finally, my Domain. My interfaces:
public interface IBoard
{
int Width { get; set; }
int Height { get; set; }
int[,] Fields { get; set; }
int WinnerId { get; set; }
void Reset();
bool IsMoveValid(int row, int column, int playerId);
IMove InsertChip(int row, int column, int playerId);
}
public interface IGame
{
GameType Type { get; }
IBoard Board { get; }
List<IPlayer> Players { get; }
bool IsMoveValid(int row, int column);
IMove MakeMove(int row, int column);
}
public interface IGameFactory
{
IGame CreateGame(GameType type, IBoard board);
}
public interface IMove
{
int PlayerId { get; set; }
bool IsConnected { get; }
bool IsGameOver { get; }
}
public interface IPlayer
{
int Id { get; set; }
string Name { get; set; }
PlayerType Type { get; set; }
}
public interface IPlayerFactory
{
IPlayer CreatePlayer(PlayerType type, int id);
}
And my implementation in Domain:
namespace Tictactoe.Domain
{
public class Game : IGame
{
private int currentPlayerIndex = 1;
public Game(GameType type, IBoard board, List<IPlayer> players)
{
Type = type;
Board = board;
Players = players;
}
public GameType Type { get; }
public IBoard Board { get; }
public List<IPlayer> Players { get; }
public bool IsMoveValid(int row, int column)
{
// It could be done entierly here. Just doing what Board now is doing: if (Board.Fields[row, column] == 0) return true;
// But there can be much more things to do in more complicated games, so I'm not sure about that
// I left that job in Board class, and just call it.
if (Players[currentPlayerIndex - 1].Type.Equals(PlayerType.Human))
{
return Board.IsMoveValid(row, column, currentPlayerIndex);
}
//if current player is Bot or Online player, your move is not allowed
return false;
}
public IMove MakeMove(int row, int column)
{
IMove result = Board.InsertChip(row, column, currentPlayerIndex);
// Set next player
if (!result.IsConnected) currentPlayerIndex = currentPlayerIndex == 1 ? 2 : 1;
return result;
}
}
}
namespace Tictactoe.Domain
{
public class Board : IBoard
{
public int Width { get; set; }
public int Height { get; set; }
public int[,] Fields { get; set; }
public int WinnerId { get; set; }
public Board(int width, int height)
{
Width = width;
Height = height;
Reset();
}
public void Reset()
{
Fields = new int[Width,Height];
for (int i = 0; i < Fields.GetLength(0); i++)
{
for (int j = 0; j < Fields.GetLength(1); j++)
{
Fields[i, j] = 0;
}
}
}
public bool IsMoveValid(int row, int column, int playerId)
{
// There will much more work to do in more complicated games
if (Fields[row, column] == 0) return true;
return false;
}
public IMove InsertChip(int row, int column, int playerId)
{
bool success = false;
bool gameOver = true;
Fields[row, column] = playerId;
// Very bad, I know. But it's just quick temp solution:
if (Fields[0, 0].Equals(playerId) && Fields[0, 1].Equals(playerId) && Fields[0, 2].Equals(playerId)) success = true;
if (Fields[1, 0].Equals(playerId) && Fields[1, 1].Equals(playerId) && Fields[1, 2].Equals(playerId)) success = true;
if (Fields[2, 0].Equals(playerId) && Fields[2, 1].Equals(playerId) && Fields[2, 2].Equals(playerId)) success = true;
if (Fields[0, 0].Equals(playerId) && Fields[1, 0].Equals(playerId) && Fields[2, 0].Equals(playerId)) success = true;
if (Fields[0, 1].Equals(playerId) && Fields[1, 1].Equals(playerId) && Fields[2, 1].Equals(playerId)) success = true;
if (Fields[0, 2].Equals(playerId) && Fields[1, 2].Equals(playerId) && Fields[2, 2].Equals(playerId)) success = true;
if (Fields[0, 0].Equals(playerId) && Fields[1, 1].Equals(playerId) && Fields[2, 2].Equals(playerId)) success = true;
if (Fields[0, 2].Equals(playerId) && Fields[1, 1].Equals(playerId) && Fields[2, 0].Equals(playerId)) success = true;
// Check if there any valid move has left
if (Fields.Cast<int>().Any(field => field == 0)) gameOver = false;
return new Move(playerId, success, gameOver);
}
}
}
namespace Tictactoe.Domain
{
// Separate class for Move result, because in more compilcated games,
// there could be more info to pass.
// Even here, UI would like to know how fields are connected (when won) - to show it (painting the line or sth)
public class Move : IMove
{
public int PlayerId { get; set; }
public bool IsConnected { get; }
public bool IsGameOver { get; }
public Move(int playerId, bool isConnected, bool isGameOver = false)
{
PlayerId = playerId;
IsConnected = isConnected;
IsGameOver = isGameOver;
}
}
}
namespace Tictactoe.Domain
{
public class Player : IPlayer
{
public int Id { get; set; }
public string Name { get; set; }
public PlayerType Type { get; set; }
public Player(PlayerType type, int id)
{
Type = type;
Id = id;
}
}
}