Jeremy.Chatelaine.Name - Battle for Independence - GameIncubator - Vivecsoft


Edito

News

Exp. Pro
Exp. Perso

Download

Prog

Design

Management

Divers

CV

Liens

Quiz

Livre d'or

Contact

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Kamron's Programming 16 Octobre 2005 - Coupling::

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.

top

Copyright 2002-2007 Jeremy Chatelaine

top