Cela fait pas mal de temps que je n'ai pas fait d'article sur la programmation,
alors je me suis que ca ferrait pas de mal de voir un peu certaines notions de
programmation, et notamment celle de coupling.
1. Introduction
Une des notions les plus importantes en programmation est de faire que notre
code soit le plus possible independant. En jeu video, comme on est des charlots,
on ne s'en soucie que bien trop peu. Avoir du code independant est moins facile
a produire, demande plus de reflection et plus de temps a mettre en place, mais
du coup on a moins l'occasion de tout casser et refaire a chaque fois car son
code n'est PAS independant... Bref, lorsqu'on a des parties dependantes les une
des autres on dit qu'on a un accouplement (coupling en anglais) et c'est mal.
2. Problematique
Le coupling est nefaste pour plusieurs raisons:
- Il entrave la reutilisation du code pour d'autres buts que celui dans lequel
il a ete cree (l'un n'existe pas sans l'autre, du coup on fait comment lorsqu'on
veut plus une partie ou en changer? Ouaip, on est dans la merde et on a becoup
de boulot pour tout changer)
- Si l'une des deux parties accouplees change, l'autre doit etre au courant et
changer egalement (effet des dominos, apres tout c'etait juste sense etre un changement
dans la librairie d'affichage comment diantre on peut se retrouver a changer du
code reseau?! haha)
- Cela rend difficile la maintenance et la comprehension du code (pour comprendre
une partie il faut egalement comprendre l'autre, mais pour comprendre l'autre
faut aussi comprendre encore cette autre la... ;))
- Empecher litterallement toute modification dans le comportement d'une librairie
lorsqu'on a pas les sources de celle ci (puisque c'est un bloque monolithique
deja compile on est fouttu, nous reste a reecrire la librairie nous meme! Pas
grave on est des warriors nous :P)
Biensur, tout cela est bien pire plus il y a de dependances entre elles (une
partie dependante de 3 autres qui dependent de 2 autres qui elles meme…
vous voyez le topo quoi).
Si vous l'avez pas encore compris, coupler c'est mal et ca vous ferra a coup sur
travailler plus que necessaire, donc trop.
3. Examples
Allez, parlons concret avec un exemple. Puis puisque je suis programmeur de
jeu, on va en prendre un truc classique dans les jeux: un couplage entre moteur
graphique et jeu.
Supposons que lorsqu'on pause le jeu, tous les sprites (images) doivent devenir
a moitier transparents.
Il y a plusieurs facon de s'aquitter de la tache. La mauvaise qui introduit
un couplage est de faire que dans votre fonction d'affichage de sprite on verifie
si le jeu est en pause ou pas et on affiche la transparence en fonction. Dans
ce cas, vous venez de creer un couplage entre votre jeu et votre moteur d'affichage.
Desormais, si vous voulez reutiliser vos sprites, ils doivent connaître
la notion de pause. Ca semble con hein? Chaque couplage que vous faite va rendre
la maintenance plus difficile et la separation bien plus douloureuse.
Beuh, faut etre nain pour faire ca moi j'fais pas de coupling dans mon code...
Ha ouais?! Le coupling est pourtant un etre vicieux qui se faufile partout
sitot qu'on y prete pas attention. Par exemple, la ligne suivante bien qu'ayant
l'air totallement inoffensive contient un couplage flagrant:
MACLASS *data = new MACLASS();
On vient d'accoupler d'emblee la fonction ou est declaree cette ligne et la
gestion d'allocation de memoire (vous utiliser new!!!).
Je vous accorde que dans cet exemple, tout les compilateurs sont fournis avec
new, donc on pourrait se dire que c'est chercher la petite bete. Certes, mais
c'est tout de meme un coupling et tout coupling pose exactement les meme problemes
cites plus haut lorsqu'il est temps de decoupler. Pour preuve, imaginez qu'a present
je desire determiner ou est alloue la memoire, que je veuille pouvoir controler
combien de memoire va dans telle partie du code ou tout simplement gerer la memoire
(pour determiner si j'ai des leak de memoire). Bah, je peux pas a moins de decoupler
cet accouplement... et croyez moi, decoupler c'est aussi facile que denouer des
noeux, a savoir long et penible! Si le couplage a lieu dans une librairie a laquelle
je n'ai pas access... ouais dans le mille vous pourrez pas faire ce que vous voulez,
vous reste plus qu'a reecrire la librairie vous meme. Quelle pouasse!
4. Afin d'eviter le coupling
Ok ok ok on a compris que c'est mal, mais on fait quoi contre?
Allez on garde espoire, l'arme la plus efficace contre le coupling est la reflexion.
Une fois qu'on a compris ce qu'etait le coupling on peut le combattre. Pour le
combattre on peut appliquer deux techniques simples:
4.1 Responsabilites.
Une des approches est de penser en responsabilites.
Reprenons l'exemple du moteur graphique et des sprites. Les responsabilites
sont claires. Le moteur graphique doit afficher ce qu'on lui demande et le jeu
doit dire quoi et comment. Si on pense en responsabilite, il devient clair que
c'est pas pas a l'affichage du sprite de voir si le jeu est en pause puisqu'on
a definie que c'est au jeu de dire quoi et comment afficher. Le role du sprite
est de s'afficher selon les desires de ceux qui l'utilisent. Si vous ajoutter
un test du jeu en pause dans l'affichage vous compromettez vos responsabilites.
Chaque classe doit avoir des responsabilites simples, j'aurais des doutes serieux
si on me presente une classe qui a plus de 4 responsabilites, c'est un signe de
mauvaise organisation de classe ou que la classe gere plus de choses qu'elle ne
devrait... donc de grandes chance d'etre couplee.
4.2 Interface.
L'autre approche, tres efficace en conjonction avec la premiere approche est
d'exterioriser les responsabilites via des interfaces.
Prenons un exemple d'une librairie d'aide au debuggage. La librairie vous permet
d'ajoutter des variables a controller et de visualiser leur valeurs en temps réel
a l'ecran. Le code a une reponsabilite claire, gerer les variables de debug qu'on
lui confie mais egalement de permettre la visualisation de ces variables! Mince
on est coince car on veut pas donner acces a notre moteur graphique, sinon on
couple les deux! Le role de notre librairie d'aide au debuggage est pas d'afficher,
mais plutot de gerer l'affichage (ce qu'on doit afficher et ou).
Dans ce cas on peut definir une interface (un contrat) dont le code se servira
pour remplir cette responsabilites d'affichage.
En code l'interface donnerait que;quechose comme ca:
// Definition d'une classe qui va etre responsable de
l'affichage pour notre librairie de debuggage
class IDebugAffichage
{
public:
virtual ~IDebugAffichage() {};
virtual void AfficheText(int positionX, int positionY,
const chat *text) = 0;
};
Sympa, on a desormais des responsabilites claires. La classe du dessus est
faite pour s'occuper de l'affichage et notre librairie de debuggage va utiliser
un objet de cette classe lorsqu'elle aura besoin d'afficher.
Mais mais mais, comment on fait pour afficher? Si j'utilise mon moteur graphique
dans la fonction je couple non? On pourrait croire que oui, mais tout depend d'OU
est implementee la fonction :) Vous allez voir.
La class IDebugAffichage est declarer dans notre librairie de debuggage car
c'est une classe faite pour notre librairie de debuggage. Ok, mais comment on
accede a notre moteur graphique a partir de cette classe et qui cree un objet
de cette classe?
La reponse est simple, celui qui a la responsabilite de lier notre moteur graphique
et notre outil de debuggage a savoir: le jeu.
Mais alors on a un couplage entre le jeu et le moteur graphique et entre le
jeu et notre librairie de debuggage, non?! Oui! Mais pas de couplage entre votre
moteur d'affichage et votre librairie de debuggage. On ne peut pas eliminer tout
couplage, quelqu'un doit connaitre les elements mais ce qui est important c'est
de se demander lorsque vous allez faire un autre jeu, ce que voulez vous reecrire?
La reponse est SEULEMENT le jeu et non pas les librairies, il en va de meme si
vous changer une librairie, vous voulez pas toucher aux autres. Donc avoir un
couplage entre jeu et librairie est "obligatoire" et le jeu joue donc
le role d'un agent de liaison qu'on remplace sans changer les outils :)
En gros ca donne ca:
1. Notre moteur graphique peut afficher du text (super moteur graphique oblige
;))
2. Notre outil de debuggage peut gerer des element de debuggage et utiliser l'interface
IDebugAffichage pour des requetes d'affichage de texte
3. Le jeu est pote avec le moteur graphique et la librairie de debuggage donc
va faire le lien entre les deux en creant un objet qui implemente IDebugAffichage
en utiliser le moteur graphique puis le passer a notre librairie de debuggage.
En code:
Pour notre moteur graphique on affiche avec ca : GIDisplay->DrawText(posx,
posy, text); C'est ce qu'on veut eviter d'appeler de notre librairie de debuggage
afin d'eviter que la librairie de debuggage et le moteur graphique soient couples
(on veut pas que notre librairie de debuggage connaisse GIDisplay par exemple,
donc pour pouvoir changer l'affichage sans changer notre librairie de debuggage).
Et non on veut pas coupler pour toutes les raisons nommees precedemment!
Grace a l'interface IDebugAffichage, notre librarie de debuggage a clairement
explique ce qu'elle avait besoin pour l'affichage (a savoir juste AfficheText).
Seul le jeu connait notre moteur graphique. On a d'emblee un couplage entre
moteur graphique et le jeu (acceptable), mais on veut pas en avoir pour nos librairies
entre elles.
Donc, le jeu declare cette classe:
class AffichagePourDebug : public IDebugAffichage //
en heritant d'un interface on dit que AffichagePourDebug implemente IDebugAffichage
{
public:
// implementation de l'interface IDebugAffichage
void AfficheTexte(int positionX, int positionY, const
chat *text)
{
GIDisplay->DrawText(positionX,
positionY, text); // hey voila notre moteur graphique, c'est
ok car le jeu le connait.
}
};
Maintenant deniere chose, le jeu a besoin de donner a nos outils de debuggage
un objet AffichagePourDebug. Comme AffichagePourDebug implemente IDebugAffichage,
notre librairie de debuggage ont pas besoin de savoir autrechose (telle que AffichagePourDebug
ou GIDisplay pour l'affichage).
Donc notre librairie de debuggage demande un pointer de type IDebugAffichage.
Ce qui va marcher puisque AffichagePourDebug derrive de IDebugAffichage.
Ca donne quelquechose comme ca:
AffichagePourDebug affichagedebug; // declaration de
notre objet quequepart dans le jeu
...
OutilDebug->Init(&affichagedebug); // faut bien donner
l'objet d'affichage a notre librairie de debuggage pour qu'elle sache quoi appeler.
Biensur notre librairie de debuggage garde un pointer sur cette objet et appelle
sa fonction AfficheTexte chaque fois qu'elle a besoin d'afficher du texte.
Genre:
MyDebugAffichage->AfficheTexte(0,0, "Pas de coupling c'est trop d'la
balle!"); // comme vous voyez notre librairie de debuggage
n'a aucune connaissance de GIDisplay
L'autre truc cool avec les interfaces c'est que le jeu peut decider de ne pas
vouloir afficher ce que la librairie de debuggage demande d'afficher ou bien encore
de deplacer l'affichage quelquepart autre car ca gene pour autre chose.
Autre exemple d'implementation de IDebugAffichage.
class AffichagePourDebug : public IDebugAffichage //
en heritant d'un interface on dit que AffichagePourDebug implemente IDebugAffichage
{
public:
int OffsetX
int OffsetY;
// implementation de l'interface
IDebugAffichage
void AfficheTexte(int positionX, int positionY, const
chat *text)
{
GIDisplay->DrawText(OffsetX
+ positionX, OffsetY + positionY, text); // affichage avec
un decallage qu'on peut controller du jeu!
}
};
Ca c'est puissant on change le comportement de la librairie sans toucher a
la librairie! C'est possible car la responsabilite du jeu est de gerer l'affichage
de la librairie de debuggage grace a une interface.
Maintenant si un jour je decide de changer de jeu ou de librairie d'affichage,
la seule partie a refaire est mon implementation de IDebugAffichage et je n'aurais
pas besoin de toucher la moindre ligne de code de ma librairie de debuggage. Et
ca j'aime! Si je decide de changer de moteur d'affichage la seule chose que j'aurais
a changer est l'implementation de IDebugAffichage. Et ca j'aime aussi! :)
Voila, ca demande un peu plus de temps et de jugotte mais si vous suivez ces
quelques conseils vous aurez bien moins de probleme sur le long terme et reecrirez
bien moins de code puisque vous aurez isole le code de maniere propre et efficace.
Vos librairies seront plus robustes, puissantes et elles pourront etre utilisees
dans d'autres cas que celle prevues initialement.
Sur ce bon code et bon refactoring! :)
Jeremy.
PS: Ce principe d'interface est utilise avec IMEMORY et IREPORT dans le GI.