6 years ago
Non... POO n'est pas un ennemi !
Vous voici dans un tutoriel pour réaliser le jeu SOKOBAN en Programmation Orientée Objet c'est-à-dire POO. Dans ce tutoriel il paraît même que nous parlerons du modèle d'architecture Modèle Vue Contrôleur c'est-à-dire MVC. Le programme est ambitieux mais ne vous faites aucune crainte, nous avancerons pas à pas et je vais essayer d'être clair.
Le tutoriel sera composé des 5 parties suivantes :
Voici pour la présentation du tutoriel.
Les pré-requis de ce tutoriel sont :
Avant de faire une partie théorique, je vais vous présenter le jeu SOKOBAN. Il s'agit d'un jeu idéal pour appréhender les concepts de la programmation orienté objet. Et oui, sinon je n'aurai pas fait ce tutoriel.
Nous jouons un gardien dans un entrepôt. Dans ce lieu se trouve des caisses que nous devons déplacer sur des zones de "chargement". C'est aussi simple que ça !
Nous allons, avant de passer à la pratique, nous écarter un peu du jeu SOKOBAN dans cette partie théorique. En effet, je vais vous présenter les concepts :
Version procédurale
Imaginons un petit jeu où l'on doit déplacer un personnage (ceci ressemble un peu à SOKOBAN...). Mais il se déplacera sur une petite carte (20 colonnes et 10 lignes) ainsi pas besoins de gérer de caméra puisqu'il s'agit de la taille de l'écran.
Ce personnage doit :
Le personnage sera représenté par un "x".
Voici le code de la version procédurale de ce petit programme (téléchargeable ici).
// TODO inclure la bibliothèque Gamebuino META // position du personnage int xCharacter; int yCharacter; void setup() { // initialiser la gambuino gb.begin(); // initialisation de la position du personnage xCharacter = 10; yCharacter = 5; } void loop() { // boucle d'attente while(!gb.update()); // effacer l'écran gb.display.clear(); manageMove(); paint(); } // Gérer les déplacement void manageMove() { // Note : // * Le test : (yCharacter > 0) permet de ne pas sortir du haut de l'écran. // * Le test : (xCharacter < 19) permet de ne pas sortir de la droite de l'écran. // * Le test : (yCharacter < 9) permet de ne pas sortir du bas de l'écran. // * Le test : (xCharacter > 0) permet de ne pas sortir de la gauche de l'écran. // Ainsi on définit les limites du personnage. if(gb.buttons.pressed(BUTTON_UP)) { if(yCharacter > 0) { yCharacter--; } } else if(gb.buttons.pressed(BUTTON_RIGHT)) { if(xCharacter < 19) { xCharacter++; } } else if(gb.buttons.pressed(BUTTON_DOWN)) { if(yCharacter < 9) { yCharacter++; } } else if(gb.buttons.pressed(BUTTON_LEFT)) { if(xCharacter > 0) { xCharacter--; } } } // Gérer l'affichage void paint() { for(int y=0 ; y<=yCharacter ; y++) { for(int x=0; x<20 || x<xCharacter ; x++) { if(x == xCharacter && y == yCharacter) { gb.display.print("x"); // afficher le personnage } else { gb.display.print(" "); // afficher une colonne vide } } gb.display.println(); // passage à la ligne suivante } }
Version orientée objet en MVC
Le MVC est un modèle d'architecture, cela sert à décrire comment le code doit être découpé. Le MVC implique qu'il y ai 3 couches :
Dans votre programme principale vous devez avoir uniquement des interactions avec le/les contrôleur(s).
De plus, imaginez que vous souhaitez faire un portage de votre programme vers un autre terminal (sur ordinateur, par exemple) et bien dans ce cas vous n'avez que la couche vue à modifier, voilà un intérêt du MVC.
Encore un avantage du MVC, la résolution de bugs :
Pour un petit programme le découpage peut-être lourd mais voyez les avantages que cela apporte.
Un concept important dans la POO est la classe. Une classe est une structure de donnée qui a :
**
Faisons un point sur les classes.
Voici un exemple de classe C++ :
// 1) Déclaration class Plan { private: int nbPieces; public: Plan(int unNbPieces); void dessiner(); }; // 2) Définition Plan::Plan(int unNbPieces) : nbPieces(unNbPieces) { } void Plan::dessiner() { for(int i=1 ; i<=nbPieces ; i++) { // dessiner la pièce... } } // 3) Instanciation et utilisation // Création d'un objet de type *Plan* Plan *maMaison = new Plan(2); // Utilisation de l'objet maMaison->dessiner();
On remarque plusieurs parties dans le code ci-dessus.
1. La déclaration
Elle définit notre classes : ses attribut et le prototype de ses méthodes. Le mot "class" est important c'est lui qui dit au compilateur que ce que l'on écrit est une classe.
Ici nous avons un attribut : nbPieces c'est un entier qui contient le nombre de pièces du plan (c'est pas une surprise, enfin j'espère !).
Ensuite, nous avons deux méthodes :
Remarque : Faites attention au point-virgule à la fin des classes, sans ce point-virgule l'IDE Arduino donnera l'erreur "new types may not be defined in a return type" à la compilation.
De plus, une chose très importante : les attributs doivent être privés. Il s'agit de l'encapsulation, un concept de la POO, qui veut que l'on interdit l'accès au attributs de classes depuis l'extérieur. Les méthodes sont en revanche publique ce qui permet de les utiliser dans le programme principale par exemple.
Enfin, la déclaration se place dans un fichier .h (pour header, Plan.h).
Le constructeur
Plusieurs choses doivent être dites à son sujet :
2. La définition
La définition du constructeur est particulière, en effet dans beaucoup de langage (ce qui reste possible en C++) il aurait fallut écrire ceci :
Plan::Plan(int unNbPieces) { nbPieces = unNbPieces; }
Le C++ a un petit sucre syntaxique qui permet de faire la même chose de la manière suivante :
Plan::Plan(int unNbPieces) : nbPieces(unNbPieces) { }
Remarque : toutes méthodes dont le constructeur doivent être préfixées par le nom de la classe, ici : Plan::.
La méthode dessiner est composée d'une boucle qui itère sur le nombre de pièces et le reste du code je le laisse pour votre imagination.
La définition se place dans un fichier .cpp (Plan.cpp pour notre exemple).
3. Instanciation et utilisation
Lorsque qu'on créé un objet à partir d'une classe on dit qu'on instancie un objet. Cet objet est une instance autrement dit c'est une maison alors que la classe est un plan. On créé ici un pointeur de Plan, en effet le type est "Plan *". Ainsi pour instancier l'objet il faut faire appel au mot-clé C++ "new". Le "2" passé au constructeur c'est le nombre de pièces de notre maison.
Enfin pour appeler une méthode (sur un pointeur) il faut utiliser le symbole suivant : "->" (ici : maMaison->dessiner()).
4. Bonus
Une représentation UML via un diagramme de classes est possible est le voici :
Remarques :
**
Revenons à notre jeu.
Maintenant que vous savez créer des classes voyons de quoi nous avons besoins pour que notre petit programme respecte le MVC. Il faut :
Comme vous le voyez par convention :
Comme vous savez créer des classes, avant de passer à la réalisation du SOKOBAN, vous pouvez essayer de réaliser une version orienté objet qui respecte le MVC de notre exemple, ne réinventez pas tout vous pouvez en effet vous inspirer de la version procédurale.
Pour vous guider voici un diagramme de classes (fait en UML) qui représente notre petit programme avec toutes les classes et méthodes nécessaires :
Si vous n'y arrivez pas, n'ayez crainte vous pouvez télécharger la version orientée objet que j'ai fait.
Remarques :
Enfin des améliorations sont possibles, effectivement il devrait être interdit d'instancier plusieurs personnages (imaginez le désordre que ça engendrerai !). Ne lisez la suite que si vous voulez avoir mal de tête ;). Pour faire cela nous pourrions utiliser le design pattern Singleton, mais ce n'est pas l'objectif de ce cours.
La pratique vous permettra de créer l'ensemble des classes et méthodes utiles au jeu SOKOBAN. Pour les méthodes nous ferrons juste la description, en effet, nous verrons la définition (le contenu) de chacune d'elle au fur et à mesure des parties qui composent ce tutoriel.
Si vous ne vous sentez pas encore prêt à franchir le cap de la POO, il existe une version procédurale du jeu SOKOBAN (cf. partie 1 par jicehel).
Je vous invite à télécharger le code source qui vous servira de base au programme, vous le trouverez ici.
Voici le diagramme de classes du code que vous devez écrire :
Remarques :
Membre statique (ou membre de classe)
On définit par membre statique un membre appartenant à la classe. Par exemple, un attribut statique aura la même valeur pour chacune des instances de la classes. Si pour une classe donnée vous avez du code commun à toutes les instances vous pouvez alors l'écrire dans une méthode statique.
Définition des méthodes
Remarques :
1. CameraModel
2. CharacterController
3. CharacterModel
4. MainController
5. MapController
6. MapModel
7. MapView
8. SpritesManager
**
Astuces
1. Méthode ne devant pas modifier l'objet
On telle méthode s'écrit suivi par le mot-clé "const", comme dans l'exemple suivant :
class Voiture { public: void reviser() const; }; void Voiture::reviser() const { }
Effectivement, il paraît absurde de modifier la voiture lorsqu'on la révise, en effet on n'a pas à modifier l'état de la voiture.
2. Pointeur
Voici un exemple d'une fonction qui prend en paramètre un pointeur de notre classe Voiture :
void detruire(Voiture * unPointeur) { }
On dit que notre type est un pointeur grâce au mot-clé "*". Ainsi unPointeur est un pointeur de Voiture.
3. Référence
Voici un exemple de fonction qui retourne une référence de Voiture :
Voiture& construire() { }
Le "&" signifie qu'on a une référence.
4. Membres statiques
Voici un exemple d'attribut statique et de méthode statique :
class Voiture { private: static int nbInstances; public: static int getNbInstances(); }; int Voiture::getNbInstances() { return Voiture::nbInstances; }
Plusieurs remarques :
**
Vous avez désormais toutes les cartes en main pour écrire les bases du Sokoban en orienté objet.
Si vous avez terminé ou si vous rencontrez des problèmes vous pouvez télécharger la solution ici.
Dans la prochaine partie, c'est-à-dire la deuxième, nous réaliserons la gestion de la caméra.
N'hésitez pas à me faire un retour : les améliorations que vous apporteriez (un regard extérieur est toujours bienvenu), les fautes, etc.
NEW 6 years ago
Bon tout d'abord merci pour ton explication de l programmation orientée objet. J'ai compris le plus gros sur les déclarations et la philosophie et j'ai hâte de voir la suite pour concrétiser tout ça. En pratiquant, je suis sûr que cela deviendra limpide avec le temps. C'est vrai qu'au début ce n'est pas évident même si j'avais vu ça durant mes études (mais ton explication vaut bien celle que j'avais eu alors et je la trouve plus courte et plus concrète).
Sinon, j'ai remarqué quelques petites fautes que je te livre ci dessous:
Juste après l'exemple de classe C++: On remarque plusieurs partit => On remarque plusieurs parties
Dans la partie "Le constructeur"
toutes méthodes dont le constructeur doivent être préfixé
=> toutes méthodes dont le constructeur doivent être préfixées
Maintenant que vous savez créé
=> Maintenant que vous savez créer
imaginé que vous ayez un Rubik's cube
=> imaginez que vous ayez un Rubik's cube
à développer l'intérêt du MVC est alors non négligeable.
=> à développer: l'intérêt du MVC est alors non négligeable.
(imaginé le désordre que ça engendrerai !).
=> (imaginez le désordre que ça engendrerai !).
Bon, prends ton temps mais donnes nous vite la suite :)
NEW 6 years ago
Merci jicehel pour ton retour j'ai apporté les modifications.
Et j'ai également fini la rédaction de cette première partie.
NEW 6 years ago
Ca serait bien d'inclure un lien qui explique ce qu'est l'UML pour ceux qui comme moi n'y connaissent rien :) La page Wikipedia a l'air pas mal : https://fr.wikipedia.org/wiki/Diagramme_de_classes
NEW 6 years ago
Merci pour ton commentaire, je vais améliorer ce point dès que possible.
NEW 5 years ago
Bonjour, merci pour cette super intro bien technique :) !
Je suis tout nouveau en c++ et je ne suis pas encore très familier avec le concept de pointeur, du coup je pose une petite question à la volée pendant que je lis :
Est-ce qu'il est obligatoire de déclarer une instance d'une classe en tant que pointeur vers sa classe ? (c'est bien ça qui se passe quand on fait Plan* maMaison = new Plan();
?)
Si oui pourquoi ??
Merci :) !
chris-scientist
5 years ago
Bonjour Codnpix,
La déclaration d'un pointeur n'est pas obligatoire, tu peux en effet écrire :
Plan maMaison = Plan(); // *
Attention, si tu utilise cette écriture tu dois alors faire des appels méthode comme ceci :
maMaison.construire(); /* comme tu peux le voir il faut utiliser le point, * et non la flèche '->'. */
* En utilisant cette écriture, tu peux écrire :
Plan maMaison(5); // en supposant que Plan est un constructeur qui prend le nombre de pièce en paramètre // ou Plan maMaison2; // ici on fait appel au constructeur Plan()
Bonne continuation ;)
NEW 5 years ago
Bonjour Codnpix,
La déclaration d'un pointeur n'est pas obligatoire, tu peux en effet écrire :
Plan maMaison = Plan(); // *
Attention, si tu utilise cette écriture tu dois alors faire des appels méthode comme ceci :
maMaison.construire(); /* comme tu peux le voir il faut utiliser le point, * et non la flèche '->'. */
* En utilisant cette écriture, tu peux écrire :
Plan maMaison(5); // en supposant que Plan est un constructeur qui prend le nombre de pièce en paramètre // ou Plan maMaison2; // ici on fait appel au constructeur Plan()
Bonne continuation ;)
Codnpix
5 years ago
Ok, merci !
Ah oui donc c'est l'opérateur flèche pour appeler une méthode sur un pointeur, et le point pour appeler sur l'instance directement..
C'est pas facile le c++ !
Merci.
Codnpix
5 years ago
Encore une autre petite question :
J'ai commencé à implémenter les fichiers .h de chaque classe en me basant sur le diagramme UML.
Je m'aide de ton repo su github pour me corriger. Et je suis tombé sur cette syntaxe un peu partout pour les signatures de méthodes de devant pas modifier l'objet (donc statiques si je comprends bien ?) :
const int * getPlayerPositions() const;
(par exemple)
Et je ne comprends pas la différence par rapport à si j'avais fait :
static const int* getPlayerPositions();
Que signifie le fait de remettre en le mot clé const
en fin de déclaration ? Est-ce équivalent au mot clé static
?
NEW 5 years ago
Ok, merci !
Ah oui donc c'est l'opérateur flèche pour appeler une méthode sur un pointeur, et le point pour appeler sur l'instance directement..
C'est pas facile le c++ !
Merci.
NEW 5 years ago
Encore une autre petite question :
J'ai commencé à implémenter les fichiers .h de chaque classe en me basant sur le diagramme UML.
Je m'aide de ton repo su github pour me corriger. Et je suis tombé sur cette syntaxe un peu partout pour les signatures de méthodes de devant pas modifier l'objet (donc statiques si je comprends bien ?) :
const int * getPlayerPositions() const;
(par exemple)
Et je ne comprends pas la différence par rapport à si j'avais fait :
static const int* getPlayerPositions();
Que signifie le fait de remettre en le mot clé const
en fin de déclaration ? Est-ce équivalent au mot clé static
?
chris-scientist
5 years ago
Ajouter const à la fin d'une déclaration comme ceci :
const int * getPlayerPositions() const;
Cela signifie que ta méthode ne peut pas modifier ton objet.
Alors que l'écriture suivante :
const int * getPlayerPositions();
Cette dernière écriture autorise la modification de ton objet, en revanche elle retourne quelques chose que tu ne peux pas modifier.
Quand je parle de modifier l'objet c'est faire quelque chose comme (dans la définition de ta méthode) :
x = 42; // avec x un attribut de ton objet
Et pour le mot-clé static ce n'est pas la même signification. Celui-ci te permet d'accéder à une méthode d'un objet sans avoir d'instance de cet objet, comme par exemple :
Mathematiques::calculSurface(/* ... */);
Si quelques chose n'est pas clair, n'hésite pas ;)
NEW 5 years ago
Ajouter const à la fin d'une déclaration comme ceci :
const int * getPlayerPositions() const;
Cela signifie que ta méthode ne peut pas modifier ton objet.
Alors que l'écriture suivante :
const int * getPlayerPositions();
Cette dernière écriture autorise la modification de ton objet, en revanche elle retourne quelques chose que tu ne peux pas modifier.
Quand je parle de modifier l'objet c'est faire quelque chose comme (dans la définition de ta méthode) :
x = 42; // avec x un attribut de ton objet
Et pour le mot-clé static ce n'est pas la même signification. Celui-ci te permet d'accéder à une méthode d'un objet sans avoir d'instance de cet objet, comme par exemple :
Mathematiques::calculSurface(/* ... */);
Si quelques chose n'est pas clair, n'hésite pas ;)