Python, POO, Tests Unitaires et Documentation
Avant de commencer
Ce TP mêle explications et phases de code.
Les explications de ce TP ne doivent pas prendre le pas sur celles de votre intervenant. Prenez les comme une base de connaissance pour plus tard, mais préférez toujours les explications orales.
- Rappels sur la POO
- Tests Unitaires
- Objets métier
- Patron de conception strategy
1 Ce que vous allez coder
Notre jeu s’inspirera de Pokémon Unite (aucune connaissance du jeu, ni de Pokémon n’est nécessaire).
En quelques mots, il y aura des Pokemons qui s’affronteront lors de Battles en lançant des Attacks.
1.1 Objets métier
Vous allez créer les objets métier pour un jeu Pokémon.
Un objet métier est une représentation d’une entité spécifique dans le contexte d’une application métier. Il encapsule les données et les comportements associés à cette entité et est souvent utilisé pour modéliser des concepts du monde réel dans le domaine de l’application.
Cela conduit à avoir des objets contenant essentiellement des attributs et très peu de méthodes autre que des getter et setter.
Pour prendre un autre exemple, voici des objets métier pour une application simple qui gère la location de vélos :
- Velo : Représente un vélo (id, type, modèle…)
- Client : Représente une personne (id, nom, prénom…)
- Location : Représente une transaction de location (id, Client, Velo, debut, fin, montant…)
1.2 Les objets métier de l’appli
Pokemon
: qui ont diverses caractéristiques et statistiques- un nom
- un niveau
- un type
- ici les types sont : Attaquant, Défenseur, Polyvalant, Soutient et Rapide (et non pas : Feu, Eau, Plante…)
- des statistiques contenues dans un objet Statistic défini ci-dessous
Statistic
: pour éviter de surcharger la classe Pokemon, leurs stats sont stockées dans un objet de la classe Statistic- hp : health points
- attack, defense, speed… : qui serviront déterminer la force de ses attaques
Attack
: différents types d’attaques dont disposeront les Pokemons (partie 3)Battle
: servira à faire s’affronter 2 Pokemons pour déterminer l’issue du combat (partie 4)
1.3 Diagramme de classes
Voici un diagramme de classes très simplifié des classes principales à coder lors de ces TP.
2 Clone du dépôt
Le code du TP se trouve sur GitHub, vous allez créer un clone local.
-
- À la main ou avec cette commande :
mkdir -p /p/Cours2A/UE3-Complements-info/TP2 && cd $_
- Elle crée l’arborescence demandée (mkdir)
- Puis elle vous positionne dans ce dossier (cd)
- À la main ou avec cette commande :
-
git clone https://github.com/ludo2ne/ENSAI-2A-complement-info-TP.git
2.1 Ouverture dans VSCode
-
- File > Open Folder
- Allez dans le dossier
P:\Cours2A\UE3-Complements-info\TP2
- Cliquez une seule fois sur 📁 ENSAI-2A-complement-info-TP
- Puis sur le bouton
-
- L’Explorer, à gauche, permet d’explorer l’arborsence des fichiers et dossiers
⚠️ Si le dossier parent n’est pas le bon, recommencez l’Open Folder où vous aurez de gros soucis par la suite !!!
-
- Terminal > New Terminal (ou CTRL + ù)
3 Modélisation et implémentation
Dans un premier temps, nous allons travailler uniquement sur les Pokemons
(la classe Statistic
est déjà codée).
Avant d’écrire du code, nous allons réfléchir à la meilleure conception possible pour réaliser nos Pokémons. Notre conception essaiera au maximum de respecter la règle suivante : faible couplage, forte cohésion.
En d’autre termes nous allons essayer de faire :
- des classes les plus disjointes possibles (faible couplage) pour qu’une modification dans une classe ne nous demande pas de modifier les autres
- tout en essayant d’avoir dans chaque classe les tâches les plus liées possibles (forte cohésion).
3.1 Première approche : le « if/elif/else »
Nous nous interessons à la méthode get_pokemon_attack_coef()
qui va servir à déterminer la puissance de l’attaque en fonction du type de Pokémon.
-
- Imaginez s’il y avait plusieurs blocs de code similaires dans notre application, et que nous devions ajouter un nouveau type
3.2 La puissance de la POO
Au lieu d’externaliser les comportements de nos Pokemons, nous allons mettre tous leurs comportements spécifiques dans des classes filles d’une super classe Pokemon
. Ceci est rendu possible grâce à deux propriétés des objets en POO :
- héritage : il est possible de spécialiser une classe existante en modifiant son comportement, ou en ajoutant de nouveaux
- polymorphisme : deux fonctions peuvent avoir le même nom mais avoir des comportements différents
En plus, comme chacun de nos Pokemons va forcement être d’un type, aucun ne sera simplement de la classe Pokemon
, cela nous permet de rendre cette classe abstraite. En définissant clairement notre classe abstraite nous allons avoir :
- Un plan pour toutes les classes qui en héritent. Cela à pour avantages de :
- Donner des informations sur la structuration du code
- Permettre de générer automatiquement les méthodes à définir
- Limiter les bug. Si on oublie une méthode, le code plante au démarrage, ce qui évite des comportements non prévus difficiles à détecter
- Donner des informations sur la structuration du code
- Une interface unique pour tous les types de Pokemons. Quelque soit le type du Pokemon, il sera considéré comme un
AbstractPokemon
partout dans le code.
-
- renommez également le fichier en
abstract_pokemon.py
- renommez également le fichier en
Vous devriez arriver à la fin à une arborescence proche de celle-ci :
ENSAI-2A-COMPLEMENTS-INFO-TP
└── src
└── business_object
├── pokemon
│ ├── abstract_pokemon.py
│ ├── attacker.py
│ ├── defender.py
│ └── all_rounder.py └── statistic.py
Pour faire une classe abstraite, utilisez le package abc
.
Voici, pour vous inspirer, un exemple de ce qui est attendu :
abstract_personnage.py
from abc import ABC, abstractmethod
class AbstractPersonnage(ABC):
def __init__(self, phrase_attaque:str, phrase_defense:str):
self._phrase_attaque = phrase_attaque
self._phrase_defense = phrase_defense
@abstractmethod # décorateur qui définit une méthode comme abstraite
def degat_attaque(self) -> int:
pass
magicien.py
from abstract_personnage import AbstractPersonnage
class Magicien(AbstractPersonnage):
def __init__(self):
super().__init__("Lance une boule de feu", "Utilise une barrière magique")
def degat_attaque(self) -> int:
return 10
Pour vous aider, voici le diagramme de classes :
3.3 Testez votre code
-
python -m pytest -v
Pour cela vous allez utiliser le package pytest
de python.
Ce package permet de réaliser des tests unitaires dans des classes séparées. L’avantage par rapport à doctest
, c’est que les tests ne “polluent” pas vos classes, et qu’il est possible de patcher certains comportements des classes.
Un exemple de test est donné dans la classe testDefenderPokemon
. Pour rappel, un test se décompose en 3 parties :
- GIVEN : création des objets nécessaires à la réalisation du test
- WHEN : appel de la méthode à tester
- THEN : vérification du résultat
Les classes de test seront organisées de la manière suivante, en reproduisant l’architecture de votre application :
ENSAI-2A-COMPLEMENTS-INFO-TP
└── src
├── business_object
│ ├── pokemon
│ │ ├── abstract_pokemon.py
│ │ ├── attacker.py
│ │ ├── defender.py
│ │ └── all_rounder.py
│ └── statistic.py
└── test
└── test_business_object
└── test_pokemon
├── test_attacker.py
├── test_defender.py └── test_all_rounder.py
- Vérifiez que le dossier parent dans l’explorer de VSCode est : ENSAI-2A-complement-info-TP
- Utilisez des chemins complets d’import plutôt que des chemins relatifs
- la racine des chemins est paramétrée au niveau du dossier
src
- exemple :
from business_object.pokemon.abstract_pokemon import AbstractPokemon
- la racine des chemins est paramétrée au niveau du dossier
- Créez des fichiers
__init__.py
(vide)- dans TOUS les dossiers que vous créez
- c’est un peu pénible mais ça peut débloquer la situation
4 L’agrégation, l’autre façon d’ajouter de la souplesse dans le code
Maintenant que nos Pokémons sont faits, nous allons y ajouter les attaques.
Notre système va devoir respecter certaines contraintes :
- Plusieurs types d’attaques vont coexister, chacune avec un mode de calcul de dégâts différent :
- Des attaques à dégâts variables séparées en 2 types :
- attaques “physiques” qui utilisent l’attaque et la défense des Pokémons
- attaques “spéciales” qui utilisent l’attaque spé et la défense spé des Pokémons
- Des attaques à dégâts fixes qui font un nombre fixe de dégâts.
- Des attaques à dégâts variables séparées en 2 types :
- Un pokémon peut avoir plusieurs attaques et le type de l’attaque doit être transparent pour le pokémon.
4.1 Attaques à dégâts fixes
Nous allons commencer par les attaques à dégâts fixes. Comme il y aura un autre type d’attaques, toutes les attaques hériterons de la classe abstraite AbstractAttack
déjà créée. Cette classe possède la méthode abstraite compute_damage() qui devra être implémentée dans les classes filles.
-
- ainsi que sa méthode
compute_damage()
qui retournera simplement la puissance (power) de l’attaque
- ainsi que sa méthode
4.2 Attaques à dégâts variables
Nous allons ensuite coder les attaques à dégâts variables. Elles utilisent la formule suivante :
\[Damage = \big ( \frac{(\frac{2*Level}{5}+2)* Power *Att}{Def*50} +2\big) *random* other\_multipliers\]
avec :
- \(Att\) : égal soit à l’attaque ou l’attaque spé du Pokemon attaquant
- \(Def\) : égal soit à la défense ou défense spé du Pokemon défenseur
- \(Power\) : la valeur de puissance de l’attaque
- \(random\) :une valeur comprise dans l’intervalle [0.85; 1]
- \(other\_ multipliers\) : les autres multiplicateurs possibles, comme le coefficient d’attaque des pokémons.
La seule différence entre attaque physique et spéciale vient des coefficients \(Att\) et \(Def\), le reste de la formule des dégâts est identique. Nous allons donc utiliser le patron de conception template method, dont voici la modélisation UML dans notre cas :
La classe AbstractFormulaAttack
va contenir les méthodes :
- compute_damage(). Cette méthode va contenir la formule de calcul des dégâts
- mais en appelant les méthodes get_attaque_stat() et get_defense_stat() pour savoir quelle statistique utiliser
- get_attack_stat() et get_defense_stat() (abstraites). Ces méthodes devront être implémentées dans les classes filles pour déterminer quelles statistiques utiliser.
5 Architecture finale
Si vous avez le temps !
Nous allons maintenant rattacher les bouts pour créer notre architecture finale :
Cette architecture permet de décorréler les attaques des pokémons et de spécifier le comportement des attaques au fur et à mesure des héritages. Les avantages sont :
- Pour la classe
AbstractPokemon
, toutes les attaques sont desAbstractAttack
. Tant qu’elles exposent la méthode compute_damage() notre programme va fonctionner. On peut ainsi facilement ajouter de nouveaux types d’attaques sans problème. - Un Pokémon peut avoir des attaques de tous les types
- Nous pouvons ajouter un système d’état comme la paralysie ou le poison assez facilement. Il faut pour cela modifier la classe
AbstractAttack
et les classes qui en héritent. Cela sera potentiellement long, mais ne demande pas de toucher à la partie “Pokémon” de notre architecture. - Une personne pourrait se concentrer sur la création des Pokémons alors qu’une autre pourrait se concentrer sur celles des attaques sans difficulté. Les deux parties du code sont relativement indépendantes, la seule zone de couplage sont les classes
AbstractPokemon
etAbstractAttack
, qui servent avant tout à définir ce qui doit être fait par les classes filles et ce qui est accessible à l’extérieur.
Le fait d’externaliser le comportement des attaques dans des classes spécifiques puis de les lier aux Pokémons via une relation d’agrégation assez souple qui permet de changer dynamiquement les attaques d’un Pokémon est le patron de conception strategy.
Vous avez la possibilité de consulter la correction soit :
- en changeant votre dépôt local de branche
- en consultant la branche adéquat sur le dépôt distant
Voici quelques commandes git utiles pour changer de branche :
# Lister toutes les branches
git branch -a
# Avant de changer de branche, créez un point de sauvegarde de votre travail
git add .
git commit -m "<message>"
# changer de branche
git checkout <nouvelle_branche> # dans le terminal, la branche courante est indiquée entre () git checkout - # pour retourner à la branche précédente