Forget the patterns (for a moment)
Certain specific problems are best solved if we don’t try to fit everything into some pattern. MVC is not a silver bullet, it is a model, a guide that helps us organize better complex systems. Layers consist of a logical division of responsibilities, but sometimes it is better to consider better the interaction between objects without using fixed roles and stereotypes.
But there are many scenarios, as I believe this to be the case, where the problem is best solved with pure object orientation. Games and other specific cases commonly fall into this category, except, of course, the user interface.
In the end, it almost always ends in Model
, but let’s forget it for a moment.
Create your API
One way to think about the problem is by using an abstraction, in this case, an interface that functions as an API and provides the functionality needed for the rest of the system.
Let’s do a modeling exercise!
Well, we’re talking about a game, composed of a chessboard and its pieces, played by two players (even if they are virtual). It seems reasonable to think about the classes:
Jogo
Tabuleiro
Peca
Jogador
To start a new game, we could require players' dice or, if it is necessary to continue a saved game, also require a board. For example:
class JogoBuilder {
Jogo comecar(Jogador j1, Jogador j2);
Jogo continuar(Jogador j1, Jogador j2, Tabuleiro t);
}
Here comes a matter of taste. I could omit the first method and consider that to start a new game would only need to pass a board in the initial state. For example:
class JogoBuilder {
Jogo iniciar(Jogador j1, Jogador j2, Tabuleiro t);
}
There could still be a parameter to define whose turn it is:
enum QuemJoga { J1, J2 }
class JogoBuilder {
Jogo iniciar(Jogador j1, Jogador j2, Tabuleiro t, QuemJoga q);
}
We’d need a way to build the board:
class TabuleiroBuilder {
Tabuleiro carregar(JogoSalvo j);
Tabuleiro criarNovo(); //tabuleiro na posição inicial
}
Thinking in terms of API, the class Jogo
needs to provide certain services for a client code, which can be a web system, desktop, mobile or even a web service. Something like this:
class Jogo {
Tabuleiro getTabuleiro();
Jogador getJogador1();
Jogador getJogador2();
QuemJoga getQuemJogaAgora();
}
The interface of Jogo
now allows us to know the state of the board, who the players are and who can make the next move. Everything necessary to represent the game. However, something is missing so that you can effectively play.
Well, the person responsible for people’s positions is clearly the Tabuleiro
. But to represent the board well, we need to know what’s on each position.
class Posicao {
byte getLinha();
byte getColuna();
Optional<Peca> getPeca(); //pode ou não haver uma peça
}
To represent the board, we could come up with something like this:
class Tabuleiro {
Posicao[][] getPosicoes(); //permite imprimir o tabuleiro
Posicao getPosicao(byte linha, byte, coluna); //permite olhar uma posição específica
}
When moving, the player must select the piece and the position to which he will move it. The selection of the part is a UI issue and we should not address this in the API. Let’s imagine that the UI stores the reference for the part at the selected position and then it can inform our API which part will be moved.
However, how can the UI determine where the part can be moved? We should make a new call to the API every time we try and issue an alert in case of an error. A different approach would be to list the possibilities of movement. For example:
class Tabuleiro {
...
Posicao[] getPossibilidadesMovimento(Posicao p);
}
The above method receives the position selected by the user and determines the possible movements. Now, the UI can do something magical like highlight possible destinations as soon as the user selects a piece to move.
If there is a need to test whether a move is valid, the API can provide another method:
class Tabuleiro {
...
boolean ehMovimentoValido(Posicao atual, Posicao desejada);
}
But in general it is not necessary to do this.
Finally, to move the piece:
class Tabuleiro {
...
void movimentarPeca(Posicao atual, Posicao nova) throws MovimentoInvalidoException;
}
Note that in the modeling above, I made two important decisions:
- Do not encapsulate the movement. An alternative would be to create a class
Movimento
to encapsulate the positions. I did not do this because I see no other attribute that can be required, but if in the future the movement could have something more than the two positions it may be an advantage to have a new object.
- Treat an invalid move as an error. I did this in this case because my API was designed to give motion options, so in theory an invalid move should not occur and is a mistake. Without, on the other hand, letting the player keep trying to change the position of the piece anywhere on the board, it might be better to trade it for a return of success or failure.
To put the icing on the cake, one last step would be to upgrade the class Jogo
with the method that allows the player to effectively play:
class Jogo {
....
void jogar(Jogador atual, Posicao atual, Posicao nova) throws MovimentoInvalidoException, NaoEhAVezDesseJogadorException;
}
The above method must validate if the current player is the owner of the piece. If everything is ok, he moves the piece and switches the turn to the other player.
A possible problem with this implementation is that Tabuleiro
is changeable, so someone could do it:
jogo.getTabuleiro().movimentarPeca(...);
This could affect the state of the board without Jogo
make the appropriate validations and update the current game status. There are two ways to resolve this:
- Do the method
Jogo.getTabuleiro
return an immutable board. This makes it possible to only affect the board by the Jogo
.
- Make every change in
Tabuleiro
create another board. This approach is more "costly" and memory terms, but it would allow us, for example, to have the history of movements.
In the case of the second approach, we could change the method movimentarPeca
of Tabuleiro
as follows:
class Tabuleiro {
...
Tabuleiro movimentarPeca(Posicao atual, Posicao nova) throws MovimentoInvalidoException;
}
So, with every movement, a new Tabuleiro
is returned with the new state and the Game now points to this new object, perhaps storing the previous one in a list, which would allow to do the replay of the game subsequently.
All ready! And at the same time nothing ready! Now it is only implement.
Now go back to the patterns
After the core the game is well modeled we can again think about how to model the system in layers and apply the MVC standard, create the control logic and the look of the application.
But what is "service layer" here? You’re talking about Controller (the C of MCV)?
– Caffé
That’s right, it’s the controller or service layer.
– Emir Marques