Atelier 01
Dans cette première séance de laboratoire, vous allez restructurer du code afin qu'il respecte le modèle MVC. Le respect de MVC sera un critère de notation pour votre soumission de TP. Cette séance de laboratoire vous fournira les compétences de base nécessaires pour organiser votre code et pratiquer la séparation des préoccupations.
Échauffement
En classe, vous avez vu comment compiler et exécuter une classe Java depuis la ligne de commande. Pour commencer, suivez les étapes suivantes :
- Téléchargez le jeu exemple.
- Extrayez le projet sur votre disque.
- Ouvrez un terminal et
cd
vers l'emplacement du projet. Entrez dans le répertoiremvc-exercise
. - Compilez le projet avec
javac *java
. - Exécutez le projet avec
java View
. - Inspectez le code de
View
avec un éditeur de texte et modifiez-le afin que la deuxième sortie ne montre plus lex
en haut à gauche.
Pour l'instant, ne pas utiliser un IDE
Bien sûr, nous allons utiliser massivement des IDEs plus tard dans le cours. Pour l'instant, il est important de comprendre où placer les fichiers sur le disque et ce qui se passe lors de la compilation du code. Rafraîchissez vos bases maintenant, je pourrais poser des questions liées à cela lors de l'examen.
MVC
Dans le cours précédent, nous avons brièvement discuté du modèle MVC, qui permet de structurer le code en modules, chaque module ayant un but spécifique.
- Modèle : Représente l'état du modèle, mais contient le moins de logique possible.
- Contrôleur : Donne le contrôle sur la modification du modèle, assure que les changements dans le modèle sont légitimes.
- Vue : Visualise l'état du modèle et transmet les demandes de modification du modèle au contrôleur.
Crédit de l'image : Documentation MVC de Mozilla
Code existant
Le code que vous venez de tester est un mini-jeu, mais
l'implémentation est négligée,
car le jeu a un modèle
et une vue
, mais la vue
a un accès complet en lecture et en écriture sur le modèle. En
d'autres termes, il n'y a pas de
contrôleur.
- Dans l'état actuel, la vue peut modifier arbitrairement les données stockées dans le modèle, tous les changements sont possibles : des changements raisonnables, ainsi que des changements absurdes.
- Dans cette séance de laboratoire, vous ajouterez un contrôleur, qui "contrôlera" comment le modèle est modifié par la vue. Finalement, en utilisant le contrôleur, la vue ne pourra pas corrompre le modèle. Le contrôleur sert donc de couche de protection.
Règles du jeu
Le code fourni représente un très simple "jeu". Un plateau de jeu 4x4 et une seule figure qui peut se déplacer.
- Le modèle représente une simple matrice booléenne 4x4.
- Les valeurs booléennes codent les cases vides (
false
), ou la case où se trouve la figure (true
). - Initialement, la figure se trouve en haut à gauche, donc seul
[0][0]
esttrue
, toutes les autres positions du tableau sontfalse
.
- Les valeurs booléennes codent les cases vides (
- Il n'y a qu'une seule figure sur le plateau, et la vue tente de la déplacer de la position initiale en haut à
gauche
(0,0)
vers la position en bas à droite(3,3)
. - Le contexte est un jeu où les figures ne doivent être déplacées que d'une case adjacente à l'autre. D'autres déplacements ne sont pas autorisés.
- Actuellement, comme le modèle n'a absolument aucune protection, et que le code exemple fourni dans la vue en profite.
- La vue pourrait suivre les règles, en déplaçant la figure vers le bas à droite, une case à la fois.
- Cependant, puisqu'il n'y a pas de contrôleur pour imposer le respect des règles, la vue peut aussi simplement ignorer les règles du jeu et placer la figure directement sur la case cible.
Exemple 1 : La vue respecte les règles du jeu et déplace la figure, une case à la fois.
// Step 1: move figure to the right
model.board[0][0]=false; // remove figure from top left field
model.board[0][1]=true; // place figure field to the right
// Step 2: move figure to the right, again
model.board[0][1]=false; // remove figure from top left field
model.board[0][2]=true; // place figure field to the right
// Step 3: move figure to the right, again
model.board[0][2]=false; // remove figure from top left field
model.board[0][3]=true; // place figure field to the right
// Step 4: move figure down
model.board[0][3]=false; // remove figure from top left field
model.board[1][3]=true; // place figure field to the right
// Step 5: move figure down, again
model.board[1][3]=false; // remove figure from top left field
model.board[2][3]=true; // place figure field to the right
// Step 6: move figure down, again
model.board[2][3]=false; // remove figure from top left field
model.board[3][3]=true; // place figure field to the right
Exemple 2 : La vue ignore les règles du jeu et place la figure directement sur la case cible.
// Move the figure directly to target position, instead of moving along a path of allowed sequential moves.
// This implementation ignores the game rules.
model.board[0][0]=false; // remove figure from top left field
model.board[3][3]=true; // place figure on bottom right field
Modifications requises
- Votre tâche est de sécuriser le jeu, afin que la vue soit forcée à jouer selon les règles.
- Avec les modifications que vous apporterez, l'exemple 2 ci-dessus ne sera plus possible.
- En détail, vous devez :
- Créer une nouvelle classe "Contrôleur"
- Ajouter 4 méthodes au contrôleur : (chacune modifiant l'état du modèle)
public void moveLeft() {...}
public void moveRight() {...}
public void moveUp() {...}
public void moveDown() {...}
- Assurez-vous que la vue utilise le contrôleur pour déplacer la figure, c'est-à-dire que la vue ne modifie plus
directement la classe
Model
.
Pourquoi ces modifications rendent-elles notre implémentation plus sûre ?
Lorsque l'on définit manuellement des champs individuels sur true
ou false
, il est facile de faire des erreurs, ou d'ignorer délibérément les règles du jeu. Utiliser des méthodes dédiées du contrôleur pour naviguer la figure est plus élégant et garantit le respect des règles du jeu.
Packages
Généralement, votre Modèle
, Vue
, et Contrôleur
ne sont pas juste une seule classe, mais plusieurs ensembles de
classes respectifs. Java offre un
concept nommé packages
pour regrouper les classes dans des structures semblables à des dossiers.
- Revisez votre code de l'exercice précédent, afin que chaque classe réside dans un dossier dédié, c'est-à-dire que votre base de code devrait ressembler à ceci :
Qu'est-ce qui a changé ?
Les classes résident maintenant dans des dossiers dédiés, nommés view
, controller
et model
.
Notez que les classes Java ne peuvent pas accéder au code d'autres packages (dossiers) sans une déclaration import
dédiée :
Exemple, View.class
nécessite une ligne supplémentaire au début :
Qu'est-ce qu'un import, au fait ? Pourquoi ne pas simplement mettre tout dans un seul dossier ?
Voici une excellente explication des imports et de leur utilisation...
Programmation défensive
Votre code fonctionne probablement maintenant. Mais est-il robuste ? Va-t-il planter dans des cas particuliers ? Peut-il encore être détourné (piraté) pour contourner les protections MVC ?
Commandes de déplacement valides
Il est temps de faire quelques tests : Le rôle du contrôleur n'est pas seulement d'assurer que votre modèle reste dans un état valide, mais aussi de garantir qu'aucune commande dangereuse ne soit exécutée.
- Que se passe-t-il si
moveRight
est appelé 4 fois, au lieu de 3 fois d'affilée ? (C'est dangereux, car cela déplacerait littéralement la figure en dehors du plateau.) - Un bon contrôleur s'assurera que votre programme ne plante pas.
- Renforcez votre code pour qu'il ne plante pas lorsque le contrôleur est utilisé pour déplacer la figure en dehors des limites du plateau.
- Changez le type de retour des méthodes du contrôleur en
boolean
et retournezfalse
lorsqu'un déplacement demandé n'est pas autorisé.
Protection de l'interface
- Dans le prochain cours, nous examinerons de plus près un concept appelé
interfaces
. - Pour l'instant, il suffit de savoir que les interfaces peuvent être utilisées pour "cacher" certaines méthodes, afin qu'elles ne puissent pas être exploitées.
Comment une interface pourrait-elle être utilisée pour sécuriser davantage l'implémentation ?
En ce moment, la vue a toujours accès au modèle. Bien que (espérons-le) vous ayez modifié votre implémentation, de sorte que le modèle soit modifié en utilisant le contrôleur, la vue a encore besoin d'un accès en lecture au modèle (simplement pour afficher les choses). Une interface peut être utilisée pour "cacher" toutes les méthodes (ou champs privés) qui modifient l'état du modèle. De cette façon, la vue peut accéder au modèle
pour lire (afficher l'état du jeu), mais elle est forcée d'utiliser le contrôleur pour les changements d'état.
Illustration
Nous pouvons définir une interface ModelReadOnly.java
:
package model;
/**
* Read only interface for the MVC model. It must not be possible to modify model state, using
* the methods offered by this interface.
*
* @author Maximilian Schiedermeier
*/
public interface ModelReadOnly {
/**
* Retruns a 2d representation of the model. Only a single field in the matrix must be true,
* and indicated the position of the figure on the board.
*
* @return 2d boolean array with exatly one field true.
*/
boolean[][] getBoard();
}
De manière similaire, nous disons à notre implémentation existante de Model
qu'elle adhère à l'interface nouvellement
définie, mais qu'elle renvoie une copie des données au lieu des données réelles. Pour toute opération d'affichage, cela
suffira. Mais si quelqu'un tente de détourner notre getter pour manipuler directement l'état du modèle (c'est-à-dire
contourner le contrôleur), ses efforts seront vains.
package model;
// This next line has been changed.
// The `implements` keyword links the `ModelReadOnly`
// interface to our implementation.
public class Model implements ModelReadOnly {
// [...]
// Here we make sure to return a *copy* of the model state,
// instead of exposing the model.
@Override
public boolean[][] getBoard() {
boolean[][] boardRepresentation = new boolean[4][4];
for (int y = 0; y < boardRepresentation.length; y++) {
for (int x = 0; x < boardRepresentation[y].length; x++) {
boardRepresentation[y][x] = false;
}
}
boardRepresentation[posY][posX] = true;
return boardRepresentation;
}
}
D'accord, mais qu'est-ce qui est gagné ?
- La
vue
peut toujours accéder aumodèle
en lecture :
ModelReadOnly model = new Model();
model.getBoard(); // <- This method is available
model.board[0][0]=false; // <-- This line is rejected by the compiler.
// ... because the ModelReadOnly interface does not grant
// access to class internals.
Cerise sur le gâteau : toString personnalisé
Avec toutes les modifications effectuées jusqu'ici, votre code est relativement robuste. Mais la vue a encore trop de connaissances sur les détails internes du modèle. Idéalement, la vue ne devrait pas connaître plus de détails sur les internes du modèle que ce qui est absolument nécessaire.
Notre vue est encore primitive, car elle se contente d'imprimer une représentation sous forme de chaîne de caractères de l'objet modèle. Cela peut être géré par le modèle !
- Implémentez une méthode
toString()
pour votre modèle et modifiez votre interfacereadOnly
, de manière à ce que la vue n'accède jamais aux détails internes de l'objet modèle.- Les méthodes
toString()
sont appelées implicitement lorsqu'on imprime un objet. Ainsi,System.out.println(model)
appellera la méthodetoString()
de la classeModel
.
- Les méthodes
- Faites en sorte que la vue imprime le modèle en invoquant implicitement
toString()
.
Halma en ligne
- Félicitations, vous avez terminé l'exercice de codage.
- Maintenant, il est temps de jouer à Halma. Vous allez implémenter Halma dans le cadre de votre projet de groupe, donc la meilleure préparation pour votre projet de groupe est de jouer quelques parties de Halma en ligne.
- Plus vous connaîtrez bien ce que vous allez implémenter, plus ce sera facile ;)
Quelques pistes
En jouant au jeu en ligne : Pouvez-vous penser à des modèles MVC à appliquer à votre futur code Halma ? Y a-t-il des parallèles avec le code que vous venez d'écrire ? Quels changements seraient nécessaires dans le modèle ? Quelles modifications du contrôleur seraient nécessaires ?
Solution
La solution pour cette séance de laboratoire est disponible sur GitLab :