Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox



Utiliser les interfaces

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par barnabe (Joachim Dornbusch), le 03 octobre 2008

L'utilisation des interfaces n'est pas toujours présente dans la culture des développeurs AS. Pourtant, elles pourraient bien souvent contribuer à résoudre des problèmes récurrents qui surviennent au cours du développement, surtout sur des projets d'une certaine ampleur.

Le cauchemar du développeur

Qui d'entre nous n'a jamais été confronté à l'une des situations suivantes ?

  • Il faut reprendre un projet d'une certaine envergure après quelques semaines d'arrêt, mais on n'est plus très sûr d'en comprendre la structure
  • On n'ose plus modifier un projet parce qu'il existe de très nombreuses relations entre toutes les classes, et on a peur que la moindre modification entraîne des conséquences en cascade.
  • On a déjà écrit une classe dans un autre projet mais elle était liée à son contexte et celui-ci n'est plus disponible : il faut en écrire une autre dans un nouveau projet.

L'utilisation des interfaces peut contribuer à atténuer - pour ne pas dire à résoudre - ces maux bien connus. Les interfaces sont en outre une condition requise pour la mise en œuvre des “design patterns”, des modèles généraux (des “paradigmes” pour ainsi dire) permettant de structurer un programme de manière efficiente.

Quel est le principal bénéfice qu'on peut attendre de cette façon de programmer? Entre tous les avantages possibles, il en est un qui selon moi s'impose : lorsqu'un projet croît en taille, sa complexité n'augmente pas - ou bien elle augmente peu. Le projet peut se développer en restant dans ses structures initiales, ce qui permet d'en garder le contrôle !

Présentation par étapes

Au commencement était le 'code spaghetti'

Lorsqu'un projet comporte plusieurs classes, qu'elles soient ou non regroupées en packages, des relations se tissent entre celles-ci : les variables et méthodes de l'une sont appelées dans les méthodes de l'autre.

On peut schématiser ce type de structuration du code ainsi (les schémas qui suivent ne se conforment pas à la norme UML et visent seulement à la clarté).

Cette façon de travailler n'est guère appropriée lorsqu'on a affaire à des projets un tant soit peu complexes :

  • le projet est peu évolutif : la moindre modification peut engendrer un bogue à l'autre extrémité du programme.
  • il peu propice à la collaboration : c'est une toile d'araignée compréhensible par son seul auteur.
  • il est peu réutilisable parce que les classes ne sont guère dissociables : toute séparation entraîne la rupture de nombreuses liaisons.

Un seul mot d'ordre : 'encapsuler'

Les langages à objets offrent des dispositifs intégrés pour mettre un peu d'ordre dans ce chaos potentiel : les modificateurs d'accès. Les deux principaux, public et private, permettent de distinguer les parties du code :

  • sont public les méthodes qui constituent l'API (application programming interface) de la classe. Ce sont les membres (variables ou méthodes) de la classe qui en permettent à d'autres classes d'en utiliser les fonctionnalités.
  • en revanche, le code qui constitue le 'mécanisme interne' de la classe a vocation à demeurer private.

(Nous n'évoquerons pas ici les modificateurs protected et internal, qui ont trait respectivement à l'héritage et aux paquetages ou packages.)

Selon un exemple rebattu, si l'on compare une classe à une voiture, les membres (méthodes ou variables) public sont des commandes disponibles sur le tableau de bord; les membres private sont 'sous le capot', ils n'ont pas vocation à être manipulés par le code client, c'est à dire le code qui va instancier la classe, appeler ses méthodes, recourir à ses fonctionnalités.

L'utilisation des modificateurs d'accès confère au code la structure suivante, plus lisible :

L'encapsulation désigne le fait que des données et des traitements sont enfermés dans la classe qui, à l'instar d'une 'boîte noire', masque son fonctionnement concret (son 'implémentation') aux autres. L'idée est qu'un morceau de programme, une fois terminé, peut fonctionner comme un paquet de code qu'on utilise sans se préoccuper de la manière dont il rend ses services.

Notons que tout débutant en actionscript qui écrit :

monMovieClip.gotoAndStop(3);

s'appuie sans le savoir sur le principe de l'encapsulation, comme M. Jourdain faisait de la prose. En effet, la méthode gotoAndStop() est partie intégrante de l'API de la classe MovieClip; elle met en branle des quantités de code que la plupart d'entre nous ne se soucient pas d'aller déchiffrer. MovieClip est une boîte noire, elle offre une API qui rend de nombreux services mais son implémentation concrète est soigneusement encapsulée.

remarque sur les variables

Dans le schéma ci-dessus, aucune variable n'a accédé au statut de membre public. En effet, pour des raisons qui pourraient être développées, les variables ne doivent jamais être accédées de façon directe en lecture ou en écriture; elles sont manipulées via des méthodes spécifiques, les getters et les setters (“accesseurs” et “mutateurs” dans la novlangue de l'informatique francophone).

On en arrive aux interfaces

Le principe de l'encapsulation, ne constitue pourtant pas la panacée. Les classes peuvent tisser d'innombrables liaisons croisées entre membres public; le cauchemar évoqué plus haut est loin d'être entièrement dissipé.

Les interfaces vont permettre de faire un pas supplémentaires vers un développement doté de structures claires, maîtrisable et évolutives. Présentons en brièvement la syntaxe :

Une interface doit être conçue comme un modèle qui déclare, sans les implémenter, un certain nombre de méthodes. Ces méthodes sont donc dépourvues de corps - pas même dotées d'accolades vides. Elles sont publiques par essence, d'où l'absence de modificateurs d'accès.

public interface IMonInterface {
		function maFonction(param:Type):TypeDeRetour;
	}

La classe qui implémente une interface le déclare dans sa signature à l'aide du mot clé 'implements'; elle est dès lors obligée d'implémenter toutes les méthodes de son interface, avec une signature rigoureusement identique, assortie du modificateur public :

public class MaClass implements IMonInterface, IMonAutreInterface {
		public function maFonction(param:Type):TypeDeRetour {
                //ici l'implémentation concrète
                }
	}

Le code dit 'client' (ici c'est MaClasseB), c'est à dire celui qui va utiliser les services fournis par cette classe, se présentera alors ainsi :

On voit que dans ce code, le principe suivant a été mis en œuvre : monA, la variable destinée à accueillir une instance de MaClasseA, n'a pas été typée selon comme un MaClasseA, mais comme un InterfaceA.

Notez que lors de la vérification des types, monA sera considéré comme une InterfaceA : aucune méthode de MaClasseA, fût-elle public, ne pourra être appelée sur monA si elle n'est pas déclarée dans InterfaceA. Cela génèrerait une erreur à la compilation. Dans un environnement comme Eclipse, taper 'monA.' (ou Ctrl + espace) fera apparaître un menu de méthodes d'InterfaceA.

Une illustration pour finir

Pour ne pas lasser le lecteur, nous terminerons en illustrant l'intérêt d'un code structuré de cette manière par un cas d'utilisation concret.

Calculez vos impôts

On veut réaliser un calculateur de prélèvements obligatoires. Mais au moment de la conception, on ne sait pas encore combien d'impôts, de taxes ou de cotisations seront pris en charge. Qui plus est, grande est la créativité fiscale du législateur !

Ainsi, le calcul concret du montant dû au Trésor Public ou à l'URSAAF sera délégué à des classes spéciales qui devront

  • accepter un certain nombre de paramètres du 'déclarant' (variable selon le prélèvement considéré) : composition du ménage, abattements divers…
  • renvoyer, bien sûr, le montant du prélèvement !

Ecrivons l'interface suivante :

public interface IPrelevement {
		function fixerParametres(param:Object):Boolean;
                //la méthode renvoie true si les paramètres        
                //sont au complet
                function calculerImpot():Number; 
	}

Ecrivons maintenant la classe impôt sur le revenu des personnes physiques :

public class  IRPP implements IPrelevement {
 
      private const TRANCHES_IMPOSITION:Array = [
      //etc.
       ];
       private var nbEnfants:uint;
 
       //éventuellement un constructeur ici
 
       public function fixerParametres(param:Object):Boolean {
                 nbEnfants=param.nbEnfants;
       //etc.
       }
       public function calculerImpot():Number {
                //ici le calcul de l'impôt
        }; 
}

Dans la classe cliente, on aura recours au code suivant :

private var cetImpot:IPrelevement = new IRPP();

ou encore

private var cetImpot:IPrelevement = new ISF();

Raffinement supplémentaire, on pourrait imaginer qu'au lieu de typer le paramètre de la méthode fixerParametres() comme un Object, on lui donne aussi un type d'interface :

public function fixerParametres(param:IDeclaration):Boolean;

On aurait alors une classe :

public class DeclarationIRPP implements IDeclaration

et une autre :

public class DeclarationISF implements IDeclaration

Le code suivant délègue les opérations particulières liées à chaque type d'impôt à la classe qui en a la responsabilité :

//déclaration et typage, ne changera pas
private var cetteDeclaration:IDeclaration;
private var cetImpot:IPrelevement;
 
//instanciation, changera selon l'impôt considéré
 
cetteDeclaration = new DeclarationIRPP(revenuImposable, nbParts, abattements);
cetImpot = new IRPP();
 
//opérations, toujours les mêmes
if (cetImpot.fixerParametres(cetteDeclaration)) montant = cetImpot.calculerImpot();
else throw new CalculImpotException("Les paramètres de la déclaration ne correspondent pas à ce qui est attendu");

Avantage de cette structuration basée sur la notion d'interface :

  • La même classe cliente pourra prendre en charge la création de contributions nouvelles sans être modifiée;
  • Il suffira alors d'écrire de nouvelles classes qui implémentent IPrelevement;
  • Le programme va donc grossir (il comptera de plus en plus de classes) mais pas se complexifier (sa structure ne change pas !);

Conclusion

Les interfaces ont d'autres mérites; elles n'ont parfois aucun contenu, et sont utilisées pour “marquer” des classes. Une telle pratique est d'un grand intérêt en relation avec le nouveau modèle évènementiel en AS3; ce serait l'objet d'un autre tutoriel que de le montrer.