mardi, juin 1 2004

Interfaces et classes virtuelles

Plongé actuellement dans le passionant ouvrage qu'est Design patterns. Catalogue des modèles de conception réutilisables (merci Francis pour le conseil ;)), je constate que la base de chaque pattern consiste en un ensemble de classes abstraites ou d'interfaces, alors que peu de programmeurs non-habitués les utilisent réellement à leur plein potentiel. C'est pourquoi j'essaie modestement dans cet article de démontrer la réelle puissance apportée par ces outils. :)

Beaucoup de programmeurs non habitués à la POO ne se servent que trés peu des interfaces, ou des classes virtuelles (quand elles sont disponibles dans le langage utilisé). Cet article se propose de cerner l'utilité de tels outils dans la programmation orientée objet.

Les codes exemples sont partagés entre du PHP5 et de l'Actionscript. En effet, alors que le premier offre la possibilité de faire des classes abstraites, l'autre est à mon sens plus lisible et permet un typage fort des types simples ! ;)

Définitions

Signature d'un fonction/méthode : Il s'agit de la "structure" d'une méthode. En gros, cela correspond aux arguments qu'elle prend en entrée, ainsi que leurs types, et le type de la valeur retournée.

Méthode abstraite (ou virtuelle) : Il s'agit d'une méthode qui ne contient pas d'implémentation. Une telle méthode oblige les classes qui vont hériter de la classe la contenant à implémenter une méthode dont la signature sera exactement la même.

Classe abstraite : Il s'agit d'une classe possédant au moins une méthode abstraite. Une classe abstraite n'est pas instanciable, du fait que l'ensemble de ses fonctionnalités n'est pas implémenté. Une classe abstraite permet de définir certaines implémentations par défaut, et à fournir la signature des méthodes non implémentées.
Pour l'Actionscript 2, Francis Bourre propose un pattern destiné à émuler les classes abstraites : http://www.tweenpix.net/archives/000316.html

Interface : L'interface est un ensemble de méthodes qui devront être implémentées dans les classes qui utilisent cette interface. Elle ressemble trés fortement à une classe abstraite qui ne contiendrait que des méthodes abstraites, et ne contient donc que des signatures de méthodes.

Différence entre classe abstraite et interface

Tout d'abord, la classe abstraite est utilisée par le biais de l'héritage. Il s'agit donc d'une relation de type "est un". Les classes qui hériteront d'une classe abstraite auront donc toutes un lien étroit concernant leur type. De plus, la plupart des langages n'autorisant plus l'héritage multiple, une classe ne peut souvent hériter que d'une seule classe, abstraite ou non. Il s'agit donc pour une classe abstraite de définir un "super type", dont d'autres types concrets pourront dériver.

Par exemple, on peut considérer que tous les véhicules à moteur se remplissent de la même manière : il suffit de mettre du carburant dans le réservoir. Tous les véhicules à moteur peuvent également être démarrés, mais chacun est démarré d'une manière qui lui est spécifique. Ainsi, il serait possible de créer une classe abstraite qui implémente le remplissage du réservoir, mais qui oblige chacune de ses sous-classes à implémenter la méthode de démarrage.

// Classe abstraite de véhicule a moteur
// Le replissage du reservoir possède une implémentation
// Par défaut
abstract class Vehicle {
   private var $_maxFuel;
   private var $_currentFuel;
   // Méthode concrète
   protected function fill($quantite) {
      // Code pour remplir le réservoir
   }
   // Methode abstraite
   // Les sous-classes devront l'implémenter
   abstract public function start();
}
// Voiture
// Heritant de VahiculeAMoteur, on est obligé d'implémenter démarrer
// Si on ne le fait pas, une erreur va survenir
class Car extends Vehicle {
   // Surcharge de la méthode abstraite
   public function start() {
      // Code pour démarrer la voiture
   }
}
L'interface elle, spécifie quelles vont être les possibilités d'une classe, sans en fournir la moindre implémentation. Elle est donc garante des méthodes d'accés à un objet dont la classe implémente cette interface. On peut dire que l'implémentation d'une interface est une relation de type "possède les fonctionnalités de". D'ailleurs, les interfaces ont souvent des noms synonymes de "capable de" (en plus du 'I' qui commence généralement leur nom) : IDragable, IViewable, etc... Par exemple, si l'on veut qu'un objet puisse être affiché, il est possible de créer une interface qui obligera la classe dont est issu l'objet d'implémenter une méthode display() ... Pour un objet qui doit pouvoir être déplacé par drag&drop, une interface pourra définir quelles sont les méthodes qui serviront à la manipuler pour cette tâche précise. // Interface définissant le comportement d'un objet
// que l'on peut déplacer à la souris
// Toute classe implémentant cette interface devra
// implémenter deux méthodes startDrag et stopDrag
interface IDragable {
   public function startDrag():Void;
   public function stopDrag():Void;
}
// Un curseur sur une jauge doit pouvoir
// être déplacé à la souris
class Cursor implements IDragable {
   public function startDrag() {
      // Code de début de déplacement
   }
   public function stopDrag() {
      // Code de fin de déplacement
   }
}
Utilisation Les interfaces et classe abstraites n'ont un véritable sens que si elles sont associées à un langage typé fortement, ou du moins qui permet d'imposer un type d'objet dans les signatures de méthodes (comme le "type hint" de PHP5). En effet, plus qu'un "carcan" imposé au développeur de classes, elles sont surtout utiles aux classes clientes, qui vont avoir besoin d'être assurée d'un certain comportement des objets qu'elles vont utiliser. En fait, pour pousser les choses plus loin, je dirait presque que pour une souplesse d'utilisation optimale, il ne faudrait utiliser que les interfaces et classes abstraites avec le typage fort. Il arrive souvent qu'un client ait besoin d'une implémentation précise, mais de toutes façons, le typage fort n'empêche pas une instance de sous-classe d'être utilisée, parfois avec une implémentation totalement différente. Ainsi, quand une méthode va afficher un objet, elle n'a pas forcément besoin de savoir de quel type est cet objet, mais uniquement de savoir si l'objet est "affichable" ! Donc, si le fait d'être affichable revient à implémenter une interface qui demande à ce qu'une méthode display() soit présente, on utilisera cette interface pour le typage fort. Et l'objet passé à la méthode peut être de n'importe quel type, on est alors sûr qu'il disposera d'une méthode display(), dont la signature est définie par l'interface. Prenons comme exemple une carte géographique. Cette carte est affichée en grand dans une fenêtre, et un ascenseur doit la déplacer. Pour cela, le déplacement de l'ascenseur recevra en paramètre l'objet qui représente la carte. Mais pour pouvoir la déplacer, il faut être certain que cette carte possède les méthodes qui vont nous permettre de la positionner. Il faut alors créer une interface imposant des méthodes d'accés. Ainsi, toute instance d'un classe qui implémente cette interface pourra être déplacée par l'ascenceur : // Interface de déplacement
interface IMovable {
   // Positionnement de l'objet
   public function moveToCoord(x:Number, y:Number);
   // Déplacement de l'objet
   public function moveByOffset(dx:Number, dy:Number);
}
// Classe d'une carte géographique
class Map implements IMovable {
   // Clip correspondant à l'affichage de la carte
   private var _targetMC:MovieClip;
   // Methode de positionnement
   public function moveToCoord(x:Number, y:Number) {
      this._targetMC._x = x;
      this._targetMC._y = y;
   }
   // Methode de déplacement
   public function moveByOffset(dx:Number, dy:Number) {
      this._targetMC._x += dx;
      this._targetMC._y += dy;
   }
}
// Scroller
class Scroller {
   function moveObject(obj:IMovable) {
      obj.moveByOffset(10,50);
   }
}

Ainsi, la classe Scroller devient appliquable à n'importe quel objet qui possède les méthodes nécessaires à son déplacement. Il peut tout aussi biuen s'agir d'un dessin ou d'un texte. Ces classes n'ont rien à voir entre elles, mais leur comportement est unifié par l'utilisation de l'interface.

Conclusion

Comme vous pouvez le constater, les interfaces et classes abstraites offrent un moyen d'assouplir votre code, le rendant ainsi plus évolutif. Le fait d'utiliser un comportement et non plus une classe dans ses typages forts permet de généraliser une instruction à un ensemble bien plus large d'objets.

Alors que les interfaces sont souvent utilisées uniquement comme structure de classes lorsqu'on les connait peu, les associer au typage fort en font un outil trés puissant !

mercredi, avril 28 2004

Communautés Flash francophone sur divan

Le Flash Festival souhaite organiser une rencontre de flasheurs à l'occasion du festival. Voici donc le mail qui a été envoyé à tous ceux qui peuvent promouvoir cette trés bonne idée ! :D

Bonjour,

Nous aimerions susciter une grande rencontre des communautés à l'occasion de la 3e édition du Flash Festival en France, festival francophone du contenu animé et interactif sur Internet, organisé au centre Pompidou le samedi 08 mai 2004 !

Nous lançons un rendez-vous (après la soirée de remises des trophées), le samedi 08 mai à partir de 22h30 et jusqu'à 02h00, au Divan du monde qui sera l'occasion aux membres de nos communautés, d'échanger de visu et de se rencontrer ! L'entrée est bien sûr libre et ouverte à tous ! (pas d'open bar ;))

Nous invitons donc les différentes communautés qui le souhaitent (Flash France, Flash-Forum, VisionFlash, Flashxpress et les blogs Lalex, TweenPix et GotoAndPlay) à communiquer sur ce rendez-vous dans leurs forums ou blogs pour que celui-ci soit une réussite et un grand moment d'échange et de rencontre !

N'hésitez pas à revenir vers nous pour toutes suggestions :)

Le Divan du monde : http://www.divandumonde.com/
Flash Festival en France : http://www.flashfestival.net/

Divan du monde (Angle des rues La Cigale / chez Michou. Métro Pigalle, ligne 2 ou 12).
Plan: par ici

Bon, je suis super déçu de pas pouvoir y aller ... mais je conseille vivement à ceux qui en ont la possibilité de s'y rendre, ca devrait être super sympa ! 8)

lundi, avril 26 2004

Erratum article sur le pathfinding A star

Patrick Lester, auteur de l'article sur le pathfinding A* que j'ai traduit, me signale qu'en lisant les commentaires fait sur ce blog à la suite de l'article, il s'est rendu compte d'une erreur, signalée par Yogaman. En effet, dans le cas où les diagonales sont utilisées, l'heuristique Manhatan peut retourner une estimation supérieure à la distance restant à parcourir. Or, pour une fiabilité complète, il faut que l'estimation de la distance à parcourir soit inférieure ou égale à la distance restant réellement à parcourir ...

Il est maintenant signalé que la méthode Manhatan est inadmissible pour l'exemple donné, mais pour des raisons de simplification, elle reste celle qui sera utilisée dans l'exemple de l'article, comme l'a signalé l'auteur dans son article original.

Mon implémentation de l'algorithme se retrouve donc avec la même erreur, faisant que le chemin retourné peut ne pas être le plus court (toujours dans le cas des diagonales autorisées) ... Il s'agit donc de modifier la méthode heuristic pour prendre en compte les diagonales ou non. C'est là que je suis bien content d'avoir fait de l'heuristique une méthode statique indépendante ! 8) A ce propos, je rappelle que le cout par défaut utilisé est de 10. Dans le cas où les couts de chaque case peuvent varier (couts des terrains, contenu dans chaque case de la carte), il faut donc mettre le coût minimum dans la propriété statique DEFAULT_COST ... :)

Voici donc la version modifiée du pathfinder pour une plus grande fiabilité. J'ai également joint la classe Grid dont je me sert maintenant (tableau à deux dimensions), et modifié un peu le package de la classe (maintenant com.lalex.game) ...

::Télécharger pathfinder.zip::

lundi, mars 1 2004

ActionScript : Les secrets des prototypes

Les prototypes sont la base de la POO avec Flash, même en AS2. C'est pourquoi j'ai considéré qu'il pouvait être sympa de faire une explication détaillée de leur fonctionnement, dans un didacticiel rédigé pour MediaBox. En fait, l'idée de ce tutoriel m'est venue il y a un petit moment deja, quand dans un éclair, j'ai fini par comprendre pourquoi la méthode d'héritage préconisée par Macromedia faisait effectivement de l'héritage .... :D

Auteur : LAlex
Mail : lalex@flash-forum.net
URL : http://www.flash-forum.net
Date de création : 26/02/2004
Version : Flash 5 / MX / MX 2004

Pré requis
:arrow: Connaître les principes de la POO
:arrow: Pour appliquer dans Flash MX 2004 : De ActionScript 1 à ActionScript 2

Introduction

Le programmation orientée objet sous Flash est dite "prototype based", soit en français "basée sur les prototypes". Ce type de programmation est opposée à la programmation dite "class based", utilisée par la plupart des langages objets célèbres, tels que le C/C++/C# ou Java. Bien qu'essayant de simuler la même syntaxe que ces derniers langages, l'ActionScript 2 reste un langage de prototypes, transformé en ActionScript 1 lors de la compilation. C'est pourquoi il est également important de connaître les rouages de cette mécanique même si l'on utilise la dernière mouture de Flash.

Bien que limitant quelques possibilités très avancées de la POO, les prototypes sont également un outil fantastique, permettant une programmation plus souple, et une compréhension plus facile du fonctionnement interne de votre code. Nous allons ici faire le tour des spécificités de la programmation à base de prototypes, et plus particulièrement celle de Flash.

1. D'ou vient le prototype ?

Nous partirons du constat que toute fonction à un prototype, qui est créé en même temps que la fonction. Le prototype est un objet, contenant des propriétés et des méthodes. En fait, il n'est pas nécessaire d'en savoir plus sur ce point. D'ailleurs, l'aide de Flash nous donne peu d'informations sur ce qu'est le prototype, et se contente de dire Dans une fonction constructeur ActionScript 1, la propriété prototype fait référence à un objet qui est le prototype de la classe construite.Nous parlons ici de fonction constructeur, c'est à dire qui va servir à instancier un nouvel objet. Or, toute fonction peut être utilisée comme constructeur. ActionScript 2 fonctionne de la même manière, et les équivalences avec AS1 sont décrites dans le tutorial "D'ActionScript 1 à ActionScript 2".

Lorsque l'on crée une fonction, on peut considérer que Flash crée automatiquement un prototype associé, de type Object :

// Création d'une fonction
myFunction = function() {
   trace("myFunction a été appellée");
}

// Instruction implicite
// myFunction.prototype = {}
2. L'opérateur new() Elément de base de la POO, l'opérateur new() sert à créer un nouvel objet à partir d'une fonction constructeur. Son fonctionnement consiste à attribuer le prototype de la fonction constructeur à un objet nouvellement crée. Examinons les différentes étapes de la création de cet objet. - L'opérateur new() crée un nouvel objet - Il initialise la propriété système "__proto__" de ce nouvel objet avec le prototype de la fonction constructeur - Il initialise la propriété système "__constructor__" de ce nouvel objet avec la fonction constructeur - Il exécute la fonction sur l'objet nouvellement crée Nous pouvons reproduire le fonctionnement de new() de la manière suivante : // Déclaration du constructeur
function MaClasse(arg) {
   trace("Instance de MaClasse créée avec le paramètre : '" + arg + "'");
}

// Instruction new "classique"
var monInstance = new MaClasse("Argument");
// Même instruction décomposée
// On crée un nouvel objet
var monInstance = new Object();
// On lui attribue le prototype de la fonction
monInstance.__proto__ = MaClasse.prototype;
// On lui attribue le constructeur
monInstance.__constructor__ = MaClasse;
// On applique la fonction constructeur à l'objet
MaClasse.call(monInstance, "Argument");
3. __proto__ et la chaîne de prototypes
Nous avons vu précédemment que la construction d'un objet consiste à mettre un prototype dans sa propriété __proto__. Il s'agit maintenant de comprendre pourquoi l'on peut accéder aux méthodes et propriétés d'un objet, alors qu'elles sont créées dans le prototype. Nous allons prendre l'exemple d'un appel de méthode. En fait, lorsque l'on accède à la propriété d'un objet, Flash va chercher tout d'abord si cette méthode existe dans l'objet lui-même. Si c'est le cas, c'est cette méthode qui va être utilisée. Dans le cas contraire, Flash va rechercher dans la propriété __proto__ de l'objet si la méthode est présente, et l'utiliser s'il la trouve. S'il ne la trouve toujours pas, étant donné que __proto__ est un objet, il contient lui-même une propriété __proto__, dans laquelle Flash va continuer sa recherche, et ainsi de suite, jusqu'à ne plus avoir de __proto__ dans lequel rechercher. Voici une illustration du fonctionnement : // Classe "maison"
function MaClasse (arg) {
   trace("Instance créée avec l'argument '" + arg + "'");
}

// Ajout d'une méthode dans le prototype
MaClasse.prototype.innerDoIt = function(arg) {
        trace("innerDoIt exécuté");
}
// Création d'une instance
monInstance = new MaClasse("Argument");
// Création d'une fonction dans l'instance
monInstance.doIt = function() {
        trace("doIt exécuté");
}
// Appel des différentes méthodes
monInstance.doIt();
monInstance.innerDoIt();
trace(monInstance.toString());
Instance créée avec l'argument 'Argument' doIt exécuté innerDoIt exécuté [object Object] Ce code a en fait exécuté les méthodes suivantes : - Constructeur - monInstance.doIt(); - monInstance.__proto__.innerDoIt(); - monInstance.__proto__.__proto__.toString() (méthode toString() de Object) Cette succession de recherche s'appelle "parcourir la chaîne de prototypes". La fin du parcours se fait lorsque la méthode est trouvée, ou lorsque la chaîne se termine. Nous reviendrons plus tard sur la raison pour laquelle la chaîne se termine.

4. L'héritage

Il existe deux méthodes pour implémenter l'héritage en ActionScript 1. L'instruction 'extends' d'AS2 est un mixage des deux, assez bizarre il faut bien le dire ... Pour rappel, l'héritage permet de profiter des fonctionnalités d'une classe mère dans une classe fille qui en hérite. Pour plus d'informations, rendez-vous sur le tutorial concernant les principes de la POO.
La méthode officielle de Macromedia
Elle consiste à faire du prototype de la classe fille une instance de la classe mère. Les instances de cette classe fille possèderont donc le prototype de la classe mère dans leur chaîne de prototypes. // Classe mère<br />
Parent = function() {<br />
      trace("Nouvelle instance de Parent");<br />
}<br />
// Classe fille<br />
Child = function() {<br />
       trace("Nouvelle instance de Child");<br />
}<br />
// La classe fille hérite de la classe mère<br />
Child.prototype = new Parent()<br />
// Instanciation de la classe fille<br />
monInstance = new Child();
Pourquoi cette instruction permet-elle de faire de l'héritage ? Penchons nous en détail dessus, en se servant de ce que nous avons vu ci-dessus concernant l'instruction new() ... Si l'on utilise la notation "équivalente" de new, nous obtiendrions : // Classe mere<br />
Parent = function() {<br />
   trace("Nouvelle instance de Parent");<br />
}<br />
// Classe fille<br />
Child = function() {<br />
   trace("Nouvelle instance de Child");<br />
}<br />
// "Traduction" de Child.prototype = new Parent()<br />
Child.prototype = new Object();<br />
Child.prototype.__proto__ = Parent.prototype;<br />
Child.prototype.__constructor__ = Parent;<br />
Parent.call(Child.prototype);<br />
// "Traduction" de monInstance = new Child()<br />
monInstance = new Object();<br />
monInstance.__proto__ = Child.prototype;<br />
monInstance.__constructor__ = Child;<br />
Child.call(monInstance);
Nous avons donc dans monInstance.__proto__ = Child.prototype, et plus haut, nous avions Child.prototype.__proto__ = Parent.prototype ... Donc, on peut en déduire que monInstance.__proto__.__proto__ = Parent.prototype ! 8) Lorsque l'on va accéder à une méthode de monInstance, conformément au parcours de la chaîne de prototypes, Flash va d'abord chercher dans monInstance, puis dans monInstance.__proto__ (prototype de Child), puis dans monInstance.__proto__.__proto__ (prototype de Parent). Nous avons donc bien de l'héritage !

La méthode plus "propre".
Dans la "traduction" ci-dessus, on peut se rendre compte que toutes les lignes ne sont pas utiles. En effet, si l'on regarde la sortie générée par ce code, on peut voir Nouvelle instance de Parent Nouvelle instance de ChildEn effet, le constructeur de Parent a été appelé, alors qu'il n'est absolument pas nécessaire. Afin d'éviter cela, une méthode d'héritage préférable consiste à utiliser les lignes de traduction, mais uniquement celles qui sont nécessaires. L'instanciation de monInstance peut être refaite par l'opérateur new(), étant donné que le principe reste le même que ci dessus : // Classe mere<br />
Parent = function() {<br />
   trace("Nouvelle instance de Parent");<br />
}<br />
// Classe fille<br />
Child = function() {<br />
   trace("Nouvelle instance de Child");<br />
}<br />
// Héritage<br />
// ** Child.prototype = new Object();<br />
// Cette ligne n'est pas nécessaire, étant donné que<br />
// le prototype de Child est crée automatiquement à la<br />
// création de la fonction<br />
Child.prototype.__proto__ = Parent.prototype;<br />
Child.prototype.__constructor__ = Parent;<br />
// ** Parent.call(Child.prototype);<br />
// Cette ligne n'est pas nécessaire : il est inutile<br />
// d'exécuter le constructeur de la classe mère<br />
// "Traduction" de monInstance = new Child()<br />
monInstance = new Child()
Ainsi, les fonctionnalités de l'héritage sont préservées, sans qu'aucune instruction inutile ne soit executée. :idea: Les spécifications du format SWF v7 donnent l'implémentation de l'instruction extends en ActionScript 2: Subclass.prototype = new Object();<br />
Subclass.prototype.__proto__ = Superclass.prototype;<br />
Subclass.prototype.__constructor__ = Superclass;
Cette implémentation a fait débat, concernant le fait qu'en executant la première ligne, à priori inutile, elle détruit le prototype actuel, détruisant ainsi le prototype courant, et la propriété "constructor" de celui-ci ... Vous pouvez avoir plus d'informations sur la discussion en français qui a eu lieu sur le devblog de LAlex.

5. Pourquoi parle-t-on de "prototypes de MovieClip" ? Lorsque l'on parle de prototypes de MovieClip comme les petits utilitaires permettant de faire un mouvement élastique ou autre, il s'agit d'un abus de langage, issu des débuts de la programmation Flash durant lesquels le vocabulaire propre à la POO n'était pas des plus répandus chez les programmeur utilisant cette technologie. En réalité, pour être exact, faire un "prototype de MovieClip" signifie "rajouter une méthode au prototype de MovieClip". Etant donné que tous les clips de Flash on un __proto__ qui pointe vers MovieClip.prototype, ajouter une méthode à ce prototype revient à ajouter une méthode a chaque clip !!!

6. La cas particulier de Object
Nous parlions plus haut de parcourir la chaîne de prototypes jusqu'à ce qu'il n'y ai plus de prototype à parcourir. La question est de savoir à quel moment s'arrête cette chaîne. Le cas particulier est en fait la classe Object. Elle a pour particularité le fait que son prototype n'a pas de constructeur. Elle possède bien un prototype, comme toute fonction constructeur, c'est pourquoi il est possible d'utiliser new Object(). Par contre ce prototype n'est issu d'aucune classe, ce qui fait que le __proto__ de Object.prototype n'existe pas. C'est comme si l'instruction implicite (vue au début de ce tutoriel) à la création de la fonction "Object" n'avait pas été exécutée. En effet, cela se vérifie facilement : // Classe interne<br />
trace("== MovieClip ==");<br />
trace(MovieClip.prototype);<br />
trace(MovieClip.prototype.__proto__);<br />
// Classe "maison"<br />
MaClasse = function() {}<br />
trace("== MaClasse ==");<br />
trace(MaClasse.prototype);<br />
trace(MaClasse.prototype.__proto__);<br />
// Classe Object<br />
trace("== Object ==");<br />
trace(Object.prototye);<br />
trace(Object.prototype.__proto__);

== MovieClip==
[object Object]
[object Object]

== MaClasse ==
[object Object]
[object Object]

== Object ==
[object Object]
undefined

C'est la raison pour laquelle la remontée de la chaîne de prototypes s'arrête à un moment donné, lorsqu'elle rencontre un __proto__ qui n'existe pas. Cette constatation nous amène aussi au fait que toutes les classes héritent forcément de Object, étant donné que leurs prototypes sont soit de type Object, soit héritent eux-mêmes de Object. C'est d'ailleurs pour ca que toutes les classes, personnelles ou intégrées à Flash, possèdent une méthode "toString"

Conclusion

Une fois les prototypes compris, se balader dans les chaînes de prototypes devient beaucoup plus clair, et l'on arrive ainsi à une compréhension accrue du fonctionnement de son code, ce qui simplifie évidemment son élaboration ainsi que son debugage.
Les prototypes peuvent également poser des problématiques inédites, véritable gymnastique de l'esprit. Les manipuler peut amener à des fonctionnalités très pointues, directement au coeur du code, traitant les classes elles-mêmes comme l'on traite des objets "normaux".

jeudi, janvier 22 2004

Quelle place pour Flash dans le développement d'applications online ?

Ma première contribution au webzine EMMA de MediaBox est un article sur la place de Flash dans les applications online. Je parle ici de Flash en tant qu'outil de développement pour des applicatifs avancés, et de la difficulté qu'à ce format à s'imposer dans le monde de l'entreprise, que ce soit pour des applications Intranet ou des gros sites dynamiques tels que Amazon.

J'y fais l'analyse non-exhaustive des raisons qui pourraient pousser à l'adopter, et des raisons qui rendent les décisionnaires souvent frileux à utiliser une telle technologie. Doit-on voir du Flash partout ? Pourquoi pas ! 8) Pourquoi n'est-ce pas le cas ? Je vous livre mon explication ... :D

lundi, septembre 15 2003

Traduction : article sur le pathfinding A*

Voici l'article qui m'a fait comprendre le pathfinding ! :D

Je l'ai traduit en français afin que le plus grand nombre puisse en profiter. L'auteur se nomme Patrick Lester et l'article original se situe à l'adresse suivante : http://www.policyalmanac.org/games/aStarTutorial.htm

Pathfinding A* pour les débutants

Alors qu'il est relativement facile à utiliser une fois que l'on a pris le coup, l'algorithme A* (prononcer "A star") peut être difficle à aborder pour des débutants. Il existe beaucoup d'articles sur le web qui expliquent A*, mais la plupart sont destinés à ceux qui en comprennent dejà les bases. Celui-ci est destiné aux vrais débutants du pathfinding.

Cet article n'est pas spécifique à un langage donc vous devrier facilement pouvoir l'adapter a n'importe quel langage de programmation.

Introduction : la zone de recherche.

Considérons que quelqu'un veut aller d'un point A à un opint B. Considérons également qu'un mur est situé entre ces deux points. Cette situation est illustrée par l'image ci-contre, où le point A (départ) est affiché en vert, le point B (arrivée) est en rouge, et où les cases en bleu représentent le mur.

La première chose que vous remarquerez est que la zone de recherche est divisée en cases. Simplifier la zone de recherche, comme nous l'avons fait ici, est la première étape en pathfinding. Cette méthode particulière réduit notre zone de recherche à un simple tableau a deux dimensions. Chaque élément du tableau représente une case de notre grille, est son statut est notifié comme étant "traversable" ou "non traversable". Le chemin va être la succession de cases à parcourir pour passer de la case A à la case B. Une fois le chemin trouvé, notre personnage va passer du centre d'une case au centre de la prochaine case, jusqu'à ce qu'il atteigne sa cible.

Ces points au centre de chaque case sont appelés "noeuds" (nodes). Quand vous lisez un texte traitant du pathfinding, vous retrouverez souvent ce terme de noeuds. Pourquoi ne pas appeler ca simplement une case ? Tout simplement parce qu'il est possible de diviser une zone de recherche en autre chose qu'une case "carrée". Cela peut-être une forme rectangulaire, ou hexagonale, ou en fait n'importe quelle forme. Ces noeuds peuvent égelement être placés n'importe ou sur les formes - au centre ou sur les contours, ou n'importe où ailleurs. Nous utilisons ici ce système tout simplement parce que c'est le plus simple ...

Commencer la recherche

Une fois la zone de recherche simplifiée à un nombre de noeuds facilement gérable, comme nous l'avons fait avec la grille ci-dessus, la prochaine étape consiste à effectuer la recherche pour trouver le chemin le plus court. En pathfinding A*, on commence cette recherche par le point A, puis en vérifiant les cases adjacentes, et en général en cherchant encore à l'extérieur jusqu'à ce que la cible soit trouvée.

Nous commencons notre recherche de la manière suivante :

Commencons au point de départ et ajoutons le a une "liste ouverte" (open list) de cases à étudier. La liste ouverte est une sorte de "shopping list". Pour l'instant, nous n'avons qu'un seul point à l'intérieur (le point A), mais il y en aura bientôt plus. Elle contient une liste de cases qui pourraient éventuellement faire partie de notre chemin, mais pas forcément. En fait, c'est une liste de pionts que nous devrons vérifier. Regardez maintenant toutes les cases ateignables (ou "traversables") adjacentes au point de départ, en ignorant les cases avec les murs, ou de l'eau, ou toute case qu'on ne peut pas traverser. Ajoutez les a la liste ouverte également. Pour chacune de ces cases, enregistrez la case A comme son étant son "parent". Cette notion de "parent" est trés importante ensuite pour pouvoir retracer le chemin. Une explication vous est donnée plus loin.
Supprimez maintenant le point A de la "liste ouverte", et ajoutez le à une "liste fermée" (closed list), qui va contenir les points que nous n'aurons plus besoin de vérifier.

A cette étape, vous devriez avoir quelquechose comme l'image ci-contre. Sur ce schéma, la carré vert foncé au centre est notre case de départ. Elle est entourée de bleu clair pour signifier que la case a bien été ajoutée à notre "liste fermée". Toutes les cases adjacentes sont maintenant dans la "liste ouverte" de case à vérifier, et sont entourées de vert clair. Chacune d'elle possède une "pointeur" vers son parent, qui n'est autre que la case de départ.

Ensuite, choisissons une case de la "liste ouvert", et répétons plus ou moins le raisonnement précédent. Mais quelle case choisir ? Celle avec le coût F le plus bas.

Cout d'un chemin.

La clé servant à déterminer quelle case utiliser parmi celles contenues dans la "liste ouverte" est l'équation suivante :
F = G + H
avec

  • G est le coût de mouvement pour aller de la case A à une case donnée sur la grille, en suivant le chemin généré jusqu'ici.
  • H est le coût de mouvement pour aller d'une case donnée sur la grille jusqu'au point de destination, le point B. Il est souvent appelé "heuristique", ce qui peut-être troublant. La raison pour laquelle on le nomme ainsi est que ce coût est estimé. En effet, nous ne connaissons pas vraiment la distance qu'il nous reste à parcourir, car toute sortes d'obstacles peuvent se trouver sur le parcours (mur, eau, etc..). Un moyen de calculer ce coût est donné dans ce tutoriel, mais il en existe d'autres que vous pourrez trouver dans d'autres articles sur le net.

Notre chemin est généré récursivement en parcourant notre "liste ouverte" et en y choisissant le case ayant le coût F le plus faible. Ce raisonnement sera abordé un peu plus loin dans cet article, mais regardons d'abord commentutiliser l'équation que nous venons de voir.

Comme décris précédemment, G est le coût de mouvement du point de départ jusqu'à la case donnée en parcourant le chamin généré jusque là. Dans cet exemple, nous allons assigner un coût de 10 pour chaque déplacement horizontal ou vertical, et un coût de 14 pour un mouvement en diagonale. Nous utilisons ces données car la distance nécessaire pour se déplacer est la racine carrée de 2 (ne prenez par peur, restez la ! ;)), ou approximativement 1.414 fois le coût d'un déplacement vertical ou horizontal. Nous utiliserons 10 et 14 pour des raisons évidentes de simplification. Le rapport est à peu prés correct, et nous évitons ainsi les calculs de racines carrées et le nombres à virgule. Mais cela n'est pas seulement pour que nous n'aimons pas les maths : utiliser des nombres entiers est aussi bien plus rapide pour l'ordinateur également. Comme vous allez bientôt le constater, le pathfinding peut être trés lent si vous n'utilisez pas ce genre d'astuces.

Etant donné que nous calculont le coût G le long d'un chemin donné, le moyen de trouver ce coût est de prendre le coût G de son parent, et de lui ajouter 10 ou 14 selon qu'il soit situé en diagonale ou pas par rapport à son parent. L'interêt de cette méthode deviendra plus clair lorsque nous serons plus loin qu'à une case du point de départ.

Le coût H peut être estimé d'un grand nombre de facons. La méthode que nous utiliserons ici est nommée la méthode "Manhattan", avec laquelle vous calculez le nombre de cases verticales et horizontales pour parvenir au point d'arrivée (en ignorant les mouvements en diagonale). Nous multiplions alors ce total par 10. Cette méthode s'appelle "Manhattan" parce qu'elle revient à calculer le nombre de patés de maisons d'un endroit à un autre, sans couper à travers un bloc en diagonale.

En lisant ceci, il est courant de penser que l'heuristique est simplement une estimation de la distance restant à parcourir à vol d'oiseau. Or, ce n'est pas le cas. Nous essayons en fait de calculer la distance restant à parcourir via le chemin à trouver (qui est généralement plus long). Plus cette estimation est proche, plus l'algorithme trouvera le chemin facilement (et rapidement). Si le chemin estimé est sur-évalué, il n'est pas certain de trouver le chemin optimal. Dans ce cas, l'heuristique est dite "inadmissible".

Techniquement, dans cet exemple, la méthode "Manhatan" est inadmissible, car elle surévalue quelque peu la distance restante. Mais nous l'utiliserons quand-même, car elle est la plus facile à comprendre dans ce cas là, et qu'il s'agit d'une surestimation assez légère. Dans les rares cas où le chemin trouvé n'est pas le plus court, il n'en sera pas trés éloigné. Si vous voulez en savoir plus, rendez vous ici.

Le coût F est calculé en ajoutant G et H. Vous pouvez constater le résultat sur le schéma ci-contre. Les coûts F, G et H sont notés dans chaque case. Comme indiqué, F est situé en haut à gauche, G en bas à gauche et H en bas à droite.

Jetons maintenant un coup d'oeil à ces cases. Dans celle qui contient les lettres, nous avons G=10. C'est parce qu'elle est située à une case du point de départ, dans une direction horizontale. Les cases en dessous et au dessus de la case de départ, ainsi que celle située à sa gauche ont toutes le même coût de 10. Les cases en diagonales ont un coût de 14.

Les coûts H sont calculés en estimant la distance "Manhattan" jusqu'à la case d'arrivée, en se déplacant horizontallement et verticalement, et en ignorant le mur qui est sur le chemin. Avec cette méthode, la case située juste à droite du départ est à trois déplacement horizontaux de l'arrivée, soit un coût H de 30. La case située juste en dessous est à quatre cases de l'arrivée (souvenez-vous, on se déplace uniquement horizontalement et verticalement) pour un coût H de 40. Vous constaterez facilement le calcul des coûts H des autres cases.

Le coût F de chaque case est calculé par une simple addition de G et H.

Continuer la recherche

Pour continuer la recherche, nous choisissone tout simplement la case ayant le coût F le plus faible parmi les case de la "liste ouverte". Puis, avec cette case nous faisons le processus suivant :

  1. Nous la supprimons de la "liste ouverte" et la rajoutons à la "liste fermée"
  2. Nous vérifions toutes ses cases adjacentes, en ignorant celles qui font partie de la "list fermée", ainsi que celle qu'on ne peut pas traverser, que nous ajoutons à la "liste ouverte" si elles n'y sont pas déjà. Assignez la case en cours comme étant le "parent" des cases nouvellement ajoutées.
  3. Si une des cases adjacentes est déjà dans la "liste ouverte", vérifiez si le chemin pour y arriver n'est pas meilleur. En d'autres termes, vérifiez si le coût G de cette case est inférieure si nous utilisons la case en cours pour y parvenir. Si ce n'est pas le cas, ne changez rien.
    D'un autre côté, si le coût G du nouveau chemin est inférieur, faites que la case en cours soit le nouveau parent de cette case adjacente (dans le schéma précédent, chagez la direction du pointeur pour pointer vers la case en cours). Pour finir, re-calculez les coût F et G de cette case. Cela semble assez confus, mais vous le verrez illustré graphiquement ci-dessous.

Bon, voyons maintenant comment cela fonctionne. Sur nos 9 cases initiales, nous en avons 8 qui font partie de la "liste ouverte" aprés que la case de départ ait été envoyée vers la "liste fermée". De ces 8 cases, celle ayant le coût F le plus faible est celle qui se situe immédiatement à droite de la case de départ, avec un coût F de 40. Donc, nous choisissons cette case comme étant la prochaine. Elle est entourée en bleu dans l'illustration ci-contre.

Tout d'abord, nous supprimons cette case de la "liste ouverte" pour l'envoyer dans la "liste fermée" (c'est pourquoi elle est maintenant entourée en bleu). Puis, nous vérifions ses cases adjacentes. Celle qui est immédiatement à droite est un mur, donc nous l'ignorons. Celle immédiatement à gauche est la case de départ. Comme elle fait partie de la "liste fermée", on l'ignore également.

Les quatre autres cases sont deja dans la "liste ouverte", donc nous devons vérifier si les chemins qui y vont sont meilleurs en passant par notre case en cour, en utilisant le coût G comme point de comparaison. Regardons la case située juste en dessous de notre case actuelle. Son coût G est de 14. Si nous y allions en passant par notre case en cours, le coût serait de 20 (10 de coût G de la case, plus 10 de déplacement vertical depuis la case en cours). Un coût G de 20 est supérieur à 14, donc le chemin en cours n'est pas meilleur. Il suffit de regarder le schéma pour que cela saute aux yeux : il est plus direct d'eller sur cette case depuis le départ en se déplacant en diagonale, plutôt qu'en se déplacant d'abord horizontalement d'une case puis verticalement d'une case.

Lorsque nous répétons ce processus pour les quatre cases adjacentes deja dans la "liste ouverte", nous nous rendons compte qu'aucun chemin n'est amélioré en passant par la case en cours, donc nous ne changeons rien. Maintenant que nous avons traité toutes les cases adjacentes, nous en avons fini avec la case en cours, et nous sommes prêts à passer à la suivante.

Donc nous parcourons a nouveau notre "liste ouverte", qui est maintenant composée de 7 cases, et nous prenons celle qui a le coût F le plus faible. Il est interessant de constater que dans ce cas, il y a deux cases avec un coût de 54. Dans ce cas, laquelle choisir ? En fait, cela a peu d'importance. Pour des raisons d'optimisation, il peut être plus rapide de choisir le dernier que vous avez ajouté à la "liste ouverte". Cela oriente la recherche en faveur des cases qui sont trouvées le plus tard au cours de la recherche, quand vous êtes arrivés plus prés de votre destination. Mais cela n'a réellement pas d'importance (différentes approches de cette notion peuvent faire que deux algorithmes A* peuvent trouver des chemins différents de longueurs égales).

Choisissons la case située juste en dessous et à droite de la case de départ, comme illustré dans le schéma ci-contre.

Cette fois-ci, quand nous vérifions les cases adjacentes, nous voyons que celle située immédiatement à droite est un mur, donc nous l'ignorons. Nous faisons la même constatation pour celle située juste au dessus. Nous ignorerons également celle qui est située en dessous du mur. Pourquoi ? Parce que vous ne pouvez pas vous y rendre directement depuis la case en cours sans passer au dessus du coin du mur. Vous avez en fait besoin d'aller vers le bas d'abord, pour ensuite vous rendre sur cette case (Note : cette règle est optionnelle. Elle dépend en fait de l'endroit ou sont situés les "noeuds").

Ceci nous laisse cing autres cases. Les deux cases situées en dessous de la case en cours ne sont pas encore dans la "liste ouverte", donc nous les ajoutons avec la case en cours comme parent. En ce qui concerne les trois autres cases, deux sont deja dans la "liste fermée" (la case de départ et celle située à sa droite, toutes les deux entourées en bleu sur le schéma), donc nous les ignorons. Et la dernière case, immédiatement à gauche de la case en cours, est vérifiée pour voir si le coût G est inférieur en passant par la case en cours. Ce n'est pas le cas, donc nous pouvons vérifier la case suivante dans notre "liste ouverte".

Nous allons répeter ce processus jusqu'à ce que la case d'arrivée soit ajoutée à la "liste fermée", ce qui nous donne quelque chose comme le schéma ci-contre.

Vous contaterez que le parent de la case située deux cases en dessous du départ a changé par rapport au précédent schéma. Avant, elle avait un coût G de 28, et pointait vers la case situé en haut à droite. Maintenant elle a un score de 20 et pointe vers la case juste au dessus d'elle. Ceci est arrivé quelque part au long de notre recherche, alors que le coût G a été vérifié et modifié pour être plus faible en utilisant un autre chemin - donc le parent a été changé, et les coûts F et G recalculés. Bien que ce changement paraisse anodin ici, il existe beaucoup de situations ou cette vérification constante va faire la différence pour déterminer le meilleur chemin jusqu'à votre destination.

Maintenant, comment déterminer le chemin lui-même ? Tout simplement en partant de la case d'arrivée, et en remontant le chemin en sens inverse d'une case à sa case parent, en suivant les flèches. Cela va vous ramener à votre chemin de départ, et voila votre chemin ! Cela devrait ressembler à l'illustration ci-contre. Se déplacer du point A ou point B consiste maintenant à vous déplacer du centre d'une case au centre de la prochaine case de votre chemin, jusqu'à ce que vous atteignez votre destination. Simple non ?!?

Récapitulatif de la méthode A*

Maintenant que nous avons effectué la totalité de nos explications, faisons un petit récapitulatif étape par étape :

  1. Ajouter le point de départ à la "liste ouverte"
  2. Répéter les instructions suivantes :
    1. Cherche la case ayant le coût F le plus faible dans la "liste ouverte". Elle devient la case en cours.
    2. Passer la case en cours de la "liste ouverte" à la "liste fermée"
    3. Pour les 8 case adjacentes à la case en cours
      • Si on ne peut pas la traverser, on l'ignore.
      • Si elle n'est pas dans la "liste ouverte", on l'y ajoute. La case en cours devient le parent de cette case. On calcule les coûts F, G et H de cette case.
      • Si elle est déjà dans la "liste ouverte", on teste si le chemin passant par la case en cours est meilleur en comparant les coûts G. Un coût G inférieur signifie un meilleur chemin. Si c'est le cas, on change le parent de la case pour devenir la case en cours, en on recalcule les coûts F et G. Si vous conservez une "liste ouverte" triée par coût F, la liste doit être retriée à ce moment la.
    4. On s'arrête quand
      • La case de destination est ajoutée à la "liste fermée"
      • Vous ne trouvez pas la case de destination et la "liste ouverte" est vide.
  3. Enregistrez le chemin. En partant de la case de destination, remontez d'un case à son parent jusqu'à atteindre la case de départ. Vous avez votre chemin ! :D

Petit apparté

Excusez moi pour ce hors-sujet, mais il est important de noter qu'à de nombreuses discussions concernant le pathfinding A* sur le web et dans des forums, vous trouverez parfois certains codes qui sont dit A*, mais ne le sont pas. Pour utiliser la méthode A*, il faut absolument inclure les notions abordées précedemment, et plus précisemment la "liste ouverte" et la "liste fermée", ainsi que le calcul des coûts F, G et H. Il existe beaucoup d'algorithmes de pathfinding, mais ces autres méthodes ne sont pas A*, qui est générallement considéré comme étant le meileur de tous. Bryan Stout aborde beaucoup d'entre eux dans cet article, incluant leurs avantages et inconvénients. Parfois, une alternative est mailleure dans certaine circonstances, mais réflechissez à ce dans quoi vous mettez les pieds!

vendredi, septembre 12 2003

Flash MX 2004 : d'ActionsScript 1 à ActionScript 2

Voici un tutoriel toujours écrit pour Flash Forum qui décrit comment passer de la POO avec ActionScript 1 (Flash MX) à la POO avec ActionScript 2 (Flash MX 2004)

Je commence par décrire comment traduire les instructions utilisées en POO avec Flash MX, presque mot à mot, vers une POO faite avec AS2. Viennent ensuite les particularité propres au langage AS2, suivie d'une brève description des habitudes qu'il faudrait prendre pour coder avec Flash MX 2004 ...

Passer de l'ActionScript 1 à l'ActionScript 2

Auteur : LAlex
Mail : lalex@flash-forum.net
URL : http://www.flash-forum.net
Date de création : 20/09/2003
Version : Flash MX 2004

Présentation

:idea: Tous les programmeurs POO en Flash MX ne sont pas forcément habitués à des languages objets tels Java ou C++. Dans sa nouvelle version MX 2004, Macromedia introduit ActionScript 2, qui a pour but, entre autre, de ralier à sa cause les programmeurs utilisant des langages "standards", en utilisant une syntaxe trés proches de celle dont ils ont l'habitude. Pour les autres, voici comment passer en douceur à l'ActionScript 2.0.
img:coupe
1. Différences des concepts

:arrow: Si vous vous interessé à ce qui se passe dans le monde de l'ActionScript (nous utiliserons l'abbréviation "AS" dans le tutoriel) vous avez peut-être deja lu ou entendu que l'AS de Flash MX permet de faire de la POO basé sur les prototypes (prototype based). A l'inverse, AS2 propose de faire de la POO basée sur les classes (class based). Voyons quels sont ces deux principes:


  • POO avec les prototypes : Pour créer une classe, on crée en fait une fonction qui sera la fonction contructeur de la classe. Ce n'est pour l'instant rien d'autre qu'une fonction. C'est le fait de l'utiliser avec l'opérateur 'new' qui en fait une classe.
    Pour rajouter une méthode à un objet, on va alors créer des fonctions dans son prototype. Lorsque l'on va appeler une méthode de l'objet, cet appel va vérifier si la méthode existe dans l'objet. Si ce n'est pas le cas, elle va regarder si elle existe dans le prototype du constructeur (puis dans le prototype du prototype, etc...) : c'est ce que l'on appelle la "chaîne de prototype". C'est comme cela qu'est gérée le fait que plusieurs objets puissent avoir la même méthode. En effet, si tous les objets ont utilisés le même constructeur, le prototype du constructeur sera le même pour tous ces objets. Pour faire de l'héritage, on rajoute tout simplement un prototype à l'endroit voulu dans la chaine de prototype. Il existe d'autres utilisations avancées des prototypes, mais nous ne somme pas la pour ca ...

  • POO avec les classes : La POO avec les classes exclu totalement la notion de prototypes. Les classes sont déclarées en tant que telles : c'est à dire que les classes ne sont pas des fonctions, mais bien des classes à part entière. C'est également dans la déclaration de la classe que nous allons créer un constructeur, les méthodes, les propriétés, etc... Une fois tout cela effectué, la classe peut être instanciée de la même manière, en utilisant l'instruction 'new'. La POO basée sur les classes simplifie également l'héritage, et propose de possibilités supplémentaires que nous verrons plus en détail une peu plus loin dans ce tutoriel.
img:coupe
2. Organisation du code en AS2

:arrow: Chaque classe créé avec AS2 doit se trouver dans un fichier .as qui porte le même nom que la classe. Vous ne pouvez pas déclarer plusieurs classes dans un même fichier .as et si le nom du fichier est différent de celui de la classe, la classe sera tout simplement ignorée (et donc impossible à instancier).

:arrow: Flash MX 2004 offre plusieurs moyens d'accéder aux classes situées dans les .as


  1. Les fichiers .as sont situés dans le même répertoire que le fichier .fla au moment de la compilation.
  2. Les fichiers sont situés dans des sous-répertoires de l'emplacement du fichier .fla et il vous faudra utiliser la commande import dans votre animation pour y accéder. Pour ceux qui connaissent, cela s'aparent au import de Java et au using de C# ... les répertoires deviennent une simulation des packages.
  3. Les fichiers .as sont dans des repertoires qui ont été rajoutés dans les paramètres de publication d'AS2 : Menu Fichier >> Paramètres de publication ... >> Onglet [Flash] >> Bouton [Paramètres...]. Ces répertoires peuvent également être utilisés comme point de départ pour les instructions import.
img:coupe
3. Portage du code de AS1 vers AS2

Les "traductions" de code qui vous sont présentées dans cette partie se limitent à offrir exactement les même fonctionnalités en AS1 que dans leur version AS2. Nous aborderons les spécificités de l'AS2 dans les parties suivantes.

  1. Déclaration d'une classe
    :arrow: Créons une classe en AS1 que l'on va nommer Individu. Nous allons la créer dans _global, afin qu'elle soit accessible de partout. Conformément à ce que nous explique fred dans son tutoriel, nous allons la déclarer comme suit :_global.Individu = function (nom, prenom, date) {
    this._nom = nom;
    this._pre = prenom;
    this._date = date;
    }
    Avec ce code, pour créer un individu, nous allons devoir lui passer un nom, un prénom et une date de naissance. Comme nous en avons l'habitude, le constructeur va s'occuper de créer les propriétés de l'objet.

    :arrow: Pour créer cette même classe en AS2, nous allons utiliser l'instruction 'class' dans un fichier nommé 'Individu.as', et créer son constructeur à l'interieur de la déclaration de classe. Voici ce que cela donne :class Individu {
    var _nom;
    var _pre;
    var _date;

    function Individu(nom, prenom, date) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    }
    }
    Vous remarquerez qu'en AS2, c'est dans la définition de la classe que l'on spécifie quelles sont ses propriétés à l'aide de l'instruction var. La fonction constructeur est une fonction qui porte le même nom que la classe, et elle se content d'assigner les valeurs aux propriétés (mais ne les crée pas). Si l'on n'avait pas déclaré les propriétés, un message d'erreur se serait affiché au moment de la compilation.
    Vous remarquerez également que nous n'avons pas utilisé le sacro-saint ciblage. En effet, il est rarement utilisé dans les langages "classiques", mais peut l'être lorsqu'il y a ambiguité sur la provenance de la variable. Le this existe donc toujours, mais est moins utilisé.

  2. Les méthodes
    :arrow: Aprés avoir créé notre classe Individu en AS1, nous allons vouloir créer ses méthodes. Pour cela, comme expliqué dans la description de la POO "prototype based", nous allons rajouter une fonction au prototype du constructeur aprés la création du constructeur :// Creation du constructeur
    _global.Individu = function (nom, prenom, date) {
    this._nom = nom;
    this._pre = prenom;
    this._date = date;
    }

    // Creation de la methode
    Individu.prototype.afficher = function() {
    trace("L'individu s'appelle " + this._pre + " " + this._nom + " et est né(e) le " + this._age + " ans.");
    }

    :arrow: Voyons maintenant comment créer cette méthode dans le cadre d'un déclaration de classe en AS2. Cette méthode va étre écrite à l'intérieur de la déclaration de classe :class Individu {
    var _nom;
    var _pre;
    var _date;

    // Fonction constructeur
    function Individu(nom, prenom, date) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    }

    // Methode afficher
    function afficher() {
    trace("L'individu s'appelle " + _pre + " " + _nom + " et est né(e) le " + _date + " ans.");
    }
    }

  3. Le addProperty et son remplacant
    :arrow: Dans les langages objets standards, les accés aux propriétés se font généralement uniquement au moyen des méthodes. addProperty est une spécificité de Flash, qui permet plus de confort pour accéder aux propriétés d'une classe. Nous pourrions ici créer une propriété "virtuelle", qui donnerait le sexe de l'individu. Il faut donc créer une méthode qui va retourner le sexe de l'individu. Considérons que pour une raison qui nous est propre, nous voulons stocker le sexe sous la forme d'un chiffre (1 pour les hommes, 2 pour les femmes), mais nous voulons y accéder avec des lettres (M pour les hommes et F pour les femmes). Il nous suffit alors de créer une propriété virtuelle "sexe" :// Creation du constructeur
    _global.Individu = function (nom, prenom, date) {
    this._nom = nom;
    this._pre = prenom;
    this._date = date;
    this._numSexe = 0;
    }

    // Methode qui retourne le sexe de l'individu
    Individu.prototype.getAlphaSexe = function() {
    switch(this._numSexe) {
    case 1 :
    return "M";
    case 2 :
    return "F";
    default :
    return "Inconnu";
    }
    }

    // Methode qui définit le sexe de l'individu
    Individu.prototype.setAlphaSexe = function(lettre) {
    switch(lettre.toUpperCase()) {
    case "M" :
    this._numSexe = 1;
    break;
    case "F" :
    this._numSexe = 2;
    break;
    default :
    this._numSexe = 0;
    }
    }

    // Ajout de la propriété 'sexe'
    with(Individu.prototype) {
    addProperty("sexe",getAlphaSexe,setAlphaSexe);
    }
    Nous avons une propriété "virtuelle" nommée 'sexe', qui va contenir soit "M", soit "F", soit "Inconnu" ... alors que notre objet stocke le sexe de l'individu sous la forme 0, 1 ou 2 ...

    :arrow: Avec AS2, nous allons pouvoir créer des méthodes qui seront considérées directement comme les méthodes get et set d'une propriété "virtuelle". Il suffit de rajouter le mot-clé 'get' ou 'set' avant le nom de la méthode :class Individu {
    var _nom;
    var _pre;
    var _date;
    var _sexe=0;

    // Fonction constructeur
    function Individu(nom, prenom, date) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    }

    // Methode qui retourne le sexe
    function get sexe() {
    switch(_numSexe) {
    case 1 :
    return "M";
    case 2 :
    return "F";
    default :
    return "Inconnu";
    }
    }

    // Methode qui définit le sexe
    function set sexe(lettre) {
    switch(lettre.toUpperCase()) {
    case "M" :
    _numSexe = 1;
    break;
    case "F" :
    _numSexe = 2;
    break;
    default :
    _numSexe = 0;
    }
    }
    }
    Et voila, nous avons effectué exactement la même chose que le code précédent en AS1, c'est à dire que nous pourrons accéder à monIndividu.sexe en utilisant "M" et "F", sauf que le addProperty n'a plus aucune utilité !

  4. L'héritage
    :arrow: En AS1, l'héritage est assez compliqué à mettre en oeuvre, car soit basé sur la méthode de Macromedia qui est assez discutable en terme d'optimisation et de logique (mais que nous allons utiliser ici par soucis de clarté), soit basé sur du code plus complexe qui exploite la chaîne de prototypes. De plus, étant donné que l'héritage se fait aprés la création du constructeur, les chances de se perdre dans le code sont assez importantes. Nous allons voir ici que l'héritage en AS2 est devenu simplissime.

    :arrow: En AS1, pour créer une classe Membre (avec login et mot de passe), qui va hériter de la classe Individu, nous allons écrire un code qui va ressembler à ca :// Constucteur de la classe Individu
    _global.Individu = function(nom, prenom, date) {
    this._nom = nom;
    this._pre = prenom;
    this._date = date;
    }

    // Constructeur de la classe Membre
    _global.Membre = function(nom, prenom, date, login, pass) {
    // On appelle le constructeur de la classe mere
    super(nom, prenom, date);
    this._login = login;
    this._pass = pass;
    }

    // On fait heriter Membre de Individu
    Membre.prototype = new Individu();
    Les instances de la classe Membre héritent ainsi des propriétés/méthodes de la classe Individu, plus celles propres à la classe Membre.

    :arrow: En AS2, l'héritage se fait le plus simplement du monde en utilisant aprés le nom de la classe l'instruction 'extends' suivi de la classe mère. N'oubliez pas que les deux classes sont dans deux fichiers différents !!!// ===== Individu.as =====

    // Declaration de la classe Individu
    class Individu {
    var _nom;
    var _pre;
    var _date;

    // Fonction constructeur
    function Individu(nom, prenom, date) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    }
    }

    // ===== Membre.as =====
    class Membre extends Individu {
    var _login;
    var _pass;

    function Membre(nom, prenom, date, login, pass) {
    super(nom, prenom, date);
    _login = login;
    _pass = pass;
    }
    }
    Et voila, difficile de faire plus simple !!! 8)

img:coupe
4. De nouvelles habitudes avec AS2

:idea: ActionScript 2 introduit des nouveautés qui n'étaient pas accessibles avec AS1, ou alors cela necessitait du code souvent lourd et trés complexe. Ces fonctionnalités deviennent quasi-obligatoires à partir du moment où l'on veut développer un code clair et rigoureux, et se doivent donc d'être utilisées dorénavant. Les exemples donnés uniquement à titre indicatif dans le paragraphe précédent ne les intégrant pas, il ne doit pas être utilisé tel quel. Voici les nouveaux concepts importants d'AS2 :

  1. Le typage "fort"
    :arrow: Le typage fort consiste à donner un type à une variable, et à lui interdire de contenir un type différent que celui qui lui a été imposé. Précédemment, il était tout à fait possible d'assigner un nombre dans une variable, puis à la ligne suivante de lui assigner une chaîne de caractères. Ce qui fait que lire un code oublié, ou fait par quelqu'un d'autre devient assez difficile s'il ne contient pas les typages ...

    :arrow: Concrétement, AS2 permet de typer les variables, les paramètres des méthodes, et la valeur de retour des méthodes. C Le typage se fait en mettant deux points ( : ) aprés la déclaration de la variable, suivi de la classe qui indique le type de la variable. Pour les valeurs retournées par les méthodes, les deux points se mettent aprés la parenthèse de fermeture des arguments. Reprenons la classe Individu créée précédemment :class Individu {
    // La déclaration des propriétés se fait avec le typage
    var _nom : String ;
    var _pre : String;
    var _date : String;
    var _numSexe : Number = 0;

    // Constructeur avec typage des arguments
    function Individu(nom:String, prenom:String, date:String) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    }

    // Methode qui renvoie le sexe avec typage de la valeur de retour
    function get sexe():String {
    switch(_numSexe) {
    case 1 :
    return "M";
    case 2 :
    return "F";
    default :
    return "Inconnu";
    }
    }

    // Le reste du code suit la meme logique ...

    }

  2. Les propriétés et méthodes publiques et privées
    :arrow: Les propriétés et méthodes d'une classe n'ont pas toutes besoins d'être accédées depuis l'extérieur de l'objet. Cela peut parfois être dangeureux d'accéder directement à une propriété, car certaines instructions peuvent ne pas être executées. AS2 offre la possibilité d'empecher l'accés de l'extérieur à certaines propriétés ou méthodes. Ainsi, seules les méthodes de l'objet peuvent accéder à ces dernières.

    :!: Ce concept est trés important, car c'est lui qui garantit l'encapsulation de vos données, ainsi que leur intégrité, surtout lors de développements en travail collaboratif, ou chacun peut-être amené à utiliser les classes d'un autre.

    :arrow: Concrétement, toute propriété/méthode qui ne spécifie pas son "accessibilité" est considérée implicitement comme étant publique. Il est quand même préférable d'éviter ce qui est implicite, est de spécifier clairement ce qui est public et ce qui est privé, excepté pour le constructeur qui est toujours public. Pour cela, on utilise les mots clés 'public' ou 'private'avant la déclaration de la porpriété ou de la méthode. Reprenons notre classe Individu :class Individu {
    // nom, prenom et date sont accessibles
    public var _nom : String;
    public var _pre : String;
    public var _date : String;

    // le sexe en numerique ne peut pas être accédé de l'exterieur
    // On utilisera plutot les methodes get et set ...
    private var _numSexe : Number;

    // Pas besoin de déclaration explicite pour le constructeur
    function Individu(nom:String, prenom:String, date:String) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    }

    // On peut aussi créer des méthodes privées
    private function maMethodePrivee():Number {
    // Cette méthode ne peut être utilisée que par une autre
    // méthode du même objet
    // ....
    }
    }

  3. Les propriétés et méthodes statiques
    :idea: Les propriétés et méthodes statiques sont partagées par toutes les instances d'un même classe. Il n'est même pas nécessaire d'avoir une instance de cette classe pour y accéder. On utilise alors la syntaxe nomDeLaClasse.nomDeLaProp ...

    :arrow: Les méthodes/propriétés statiques sont déclarées au moyen du mot-clé static, situé aprés la déclaration public/private, et avant l'instruction var/function ... Si la déclaration d'un propriété statique est accompagné d'une assignation de valeur, cette assignation n'est executée qu'une fois, au moment de la déclaration de la classe (et non pas à son instanciation).

    :arrow: Une utilisation fréquente des propriétés statiques peut être de compter le nombre d'individus :class Individu {
    // Propriétés contenant le nombre total d'individus
    // Elle est mise à zéro la première fois
    public static nbIndividu:Number = 0;

    // Propriétés non statiques
    public var _nom : String;
    public var _pre : String;
    public var _date : String;
    private var _numSexe : Number;

    // Constructeur de la classe
    function Individu(nom:String, prenom:String, date:String) {
    _nom = nom;
    _pre = prenom;
    _date = date;
    // A la création d'un individu, on augmente la propriété statique de 1
    nbIndividu++;
    }
    }

    On pourra alors accéder à cette propriété soit en appelant nbIndividus depuis l'intérieur d'un méthode (comme cela est fait dans le constructeur, soit en tapant :trace("Nombre d'individus créés : " + Individu.nbIndividus);Dans ce cas précis, le problème est que nous ne pouvons pas réduire le nombre d'individus quand un individu est supprimé, et cela parce qu'il n'est pas possible de créer un destructeur en AS2 ...


  4. Les interfaces
    :idea: Comme expliqué dans le tutoriel sur les concepts de la POO, les interface sont un peu similaires à des classes, sauf que l'on ne peut pas créer d'instance d'un interface, et que les méthoeds ne contiennent pas de code. Elle sont en fait un "contrat", que les classes qui en découlent devront impérativement remplir. Les avantanges des interfaces sont importants lorsqu'il s'agit de travailler en équipe, ou constituent tout simplement une aide à la compilation, afin d'être sur que certaines classes ont été correctement implémentées. Si dans une classe dérivée d'une interface, il manque une méthode de l'interface, le compilateur va signaler une erreur. De la même manière, si la méthode de la classe et celle de l'interface n'ont pas la même signature (nombre et type des arguments + type du retour), le compilateur va également signaler une erreur ...

    :arrow: Un autre aspect des interfaces est que l'on peut créer une classe à partir de plusieurs interfaces. Cela permet de s'assurer que la classe va bien avoir les fonctionnalités qui sont décrites dans les interfaces ...

    :arrow: On ne peut pas tout faire dans une interface AS2 :


    • On ne peut pas déclarer de propriétés
    • On ne peut déclarer que des méthodes publiques
    • On ne peut déclarer de méthodes statiques
    • Une interface ne contient pas de constructeur
    • Une interface ne peut pas implémenter une autre interface
    • Une interface ne peut pas hériter d'une classe

    :arrow: Pour déclarer une interface, on utilise la même syntaxe que pour les classes, sauf que l'on utilise le mot clé interface au lieu de class. Pour dériver une classe d'un interface, on utilise l'instruction implements aprés l'instruction d'héritage. Ensuite, on donne la liste des interfaces implémentées par la classe, separées par des virgules. Concrétement, voici un exemple de deux interfaces (toujours chacune dans un fichier .as différent) et d'un classe qui les implémente. On veut que l'individu soit un humain (il doit donc savoir parler et marcher par exemple), et l'on veut aussi qu'il soit un coureur (il doit donc savoir courir) :// ===== Humain.as ====
    interface Humain {
    public marcher(nbPas:Number);
    public parler(sujet:String):String;
    }

    // ===== Coureur.as =====
    interface Coureur {
    public courir(distance:Number);
    }

    // ===== Individu.as =====
    class Individu implements Humain, Coureur {
    // Declaration des propriétés
    var _nom:String;
    var _pre:String;
    var _date:String

    // Constructeur de la classe
    function Individu (nom:String, prenom:String, date:String) {
    // Code du constructeur
    }

    // On écrit le code des méthodes décrites par les interfaces
    // Interface Humain
    public function marcher(nbPas:Number) {
    // Instructions pour faire marcher l'individu
    }

    public function parler(sujet:String):String {
    // Instructions pour faire parler l'individu
    // sur un sujet donné. La phrase qu'il doit prononcer
    // est retournée par la méthode
    }

    // Interface Coureur
    public function courir(distance:Number) {
    // Instruction pour faire courir l'individu
    // sur une distance donnée
    }
    }
    Dans ce cas précis, si l'on avait pas créé les méthodes marcher, parler et courir, ou si l'on avait créé des méthodes qui possédaient une signature différente de celles données dans les interfaces, le compilateur aurait signalé une erreur.

  5. Les classes "dynamiques"
    :arrow: Comme cela a été dit dans la partie sur les déclarations de classes, il faut déclarer les propriétés d'une classe. Seulement, il peut arriver que l'on ai besoin de rajouter une propriété dans un objet, sans qu'elle ai forcément été déclarée avec la classe. Il faut alors autoriser ce type d'action au moment de la déclaration de la classe, au moyen du mot clé dynamic, situé devant le mot clé class.

    :arrow: Voici un exemple de script qui génère une erreur, et le même script avec une classe dynamique qui ne génère pas d'erreur :// Le constructeur va provoquer une erreur de compilation
    class Individu {
    var _nom:String;
    var _pre:String;
    var _age:Number;

    function Individu(nom:String, prenom:String, age:Number) {
    _nom = nom;
    _pre = prenom;
    _age = age;

    // La propriété _statut n'a pas été déclarée
    // La compilation va retourner une erreur
    if (age < 18) {
    _statut = "mineur";
    } else {
    _statut = "majeur";
    }
    }
    }

    // La classe etant dynamique, le contructeur ne provoque pas d'erreur
    dynamic class Individu {
    var _nom:String;
    var _pre:String;
    var _age:Number;

    function Individu(nom:String, prenom:String, age:Number) {
    _nom = nom;
    _pre = prenom;
    _age = age;

    // La propriété _statut n'a pas été déclarée
    // mais la classe est dynamique, donc pas d'erreur
    if (age < 18) {
    _statut = "mineur";
    } else {
    _statut = "majeur";
    }
    }
    }
    Les propriétés ainsi créées "a la volée" sont automatiquement publiques.


img:coupe
5. Conclusion
:arrow: Comme vous l'avez sûrement constaté, ActionScript 2 apporte non seulement une lisibilité et une organisation bien plus grande de votre code et en facilité grandement la réutilisation.

:arrow: Mais en plus de cela, ses possibilités résolument proche des langages objets standards tels que Java ou C# vous permettent aussi de créer un code plus robuste, qui s'il est bien fait, limite énormément les possibilités d'erreur à l'utilisation des classes que vous avez crées : une fois une classe créée et débuguée en AS2, vous pouvez la réutiliser à volonté, sans vous poser la question de savoir ou se situe l'erreur : elle est dans l'utilisation des classes, et non pas dans le code des classes ...

:arrow: La facilité d'utilisation de l'héritage est un des gros points forts de l'AS2. Pensez-y avant de modifier une classe que vous avez déjà développé : étendez la plutôt au moyen d'une autre classe qui en hérite ...

img:coupe img:remarque msg:tut

FAQ
Cette FAQ sera mise à jour au fur et à mesure des questions sur le forum

  • Que sont devenus mes prototypes ? Puis-je encore les utiliser ?
    Oui ... et non. Les prototypes tels qu'ils sont utilisés couramment (rajouter des fonctionnalités à une classe deja existante) sont toujours disponibles, car AS2 est compatibles avec les objets créés en AS1, ainsi qu'avec les prototypes. Seulement, la nouvelle manière de programmer introduite par AS2 devrait plus vous pousser à faire de l'héritage, c'est-à-dire créer vos propres classes à partir des classes existantes pour y ajouter vos propres fonctionnalités, plutôt que de rajouter vos fonctionnalités à la classe existante. Si ce n'est pas évident pour les classes "visuelles" (MovieClip, TextField, etc...), ca l'est beaucoup plus pour les autres...
  • Dois-je recoder toutes mes classes faites avec AS1 ?
    Non, mais c'est conseillé. En effet, comme dit plus haut, les classes AS1 sont compatibles avec les classes AS2. Seulement, la lisibilité du code risque d'en pâtir fortement. Autant uniformiser votre code le plus possible.
  • Ne serait-ce pas plus pratique de ne faire que des classes dynamiques ?
    Absolument pas. Le but de la déclaration des propriétés est d'être sûr de ne pas surcharger un objet qui n'en a pas besoin. Ne faire que des classes dynamiques reviendrait à refaire de l'AS1, sans aucun contrôle sur les classes.
  • Comment faire pour pouvoir passer n'importe quel type d'objet à une méthode avec le typage fort ?
    Il suffit de faire le typage avec Object. En fait, le typage vérifie si l'argument passé est bien du type demandé ou s'il hérite de ce type.

Interceptions d'erreurs avec Flash MX 2004

Toujours pour Flash Forum, j'ai rédigé un tutoriel sur la manière d'intercepter les erreurs et de créer ses propres erreurs avec le nouveau Flash MX 2004

On constate alors que les instructions pour emettre ou intercepter une erreur envoient en fait des instances de la classe Error ou d'une classe qui en hérite. Mais en fait, il n'existe aucune contrainte en ce qui concerne le type d'objet envoyé ou receptionné par ces instructions. On peut alors apparenter ce système avec la programmation évenementielle. De la a y voir d'autres applications, il n'y a qu'un pas ...

Interception d'erreurs dans Flash MX 2004

Auteur : LAlex
Mail : lalex@flash-forum.net
URL : http://www.flash-forum.net
Date de création : 20/09/2003
Version : Flash MX 2004

Présentation
:idea: ActionScript 2.0 nous permet maintenant d'intercepter une erreur qui aurait pu se produire durant une portion de code, voire même de créer nos propres erreurs, et de réagir différemment selon le type d'erreur. Nous allons ici voir comment s'en servir.

img:coupe
1. Intercepter une erreur

:arrow: Les instructions pour intercepter une erreur sont try, catch et finally. Ces instructions sont utilisées avec des groupes de codes entre accolades { et }, comme on le fait avec les boucles ou les fonctions :

  • try permet de commencer un portion de code dans laquelle on va pouvoir intercepter une erreur.
  • catch commence une portion de code qui va être executée si une erreur est interceptée dans la portion try qui la précède. catch fonctionne un peu comme un evenement, c'est-à-dire qu'elle prend un argument qui est de type Error.
  • finally commence une portion de code qui sera executée quoi qu'il arrive, qu'une erreur soit interceptée ou pas, même si la portion try possède un return, alros que normalement toutes les instructions appellées aprés avoir effectué un return ne sont pas executées.
:arrow: Un bloc try doit être suivi de au moins un bloc catch ou un bloc finally.

:arrow: La classe Error contient un constructeur, deux propriétés et une méthode:

  • Error( [message] ) : le constructeur prend en paramètre (optionnel) le message d'erreur.
  • Error.message : spécifie le message d'erreur, le plus souvent destiné à être affiché.
  • Error.name : Le nom de l'erreur. Ce nom peut servir d'identifiant
  • Error.toString() : Methode qui retourne une représentation de l'erreur sous forme de chaîne de caractères.
:arrow: Voyons un exemple classique d'interception d'erreur dans le cadre d'un entretien d'embauche :// On commence a surveiller les erreurs
try {
   candidat = new Individu("DURAND","Pierre","18/06/1987");
   candidat.getPermisDeConduire();
   candidat.getCurriculum();
   // Les autres instructions a effectuer
   // pour l'entretien
} catch (catchedError : Error) {
   // Si une erreur se produit, elle est tranmise à catch
   // dans le parametre catchedError
   trace(catchedError.toString());
   candidat.allerALaPorte();
} finally {
   // Quoi qu'il arrive, on supprime le candidat
   // a la fin de l'entretien
   delete postulant;
}
Ici, quand on demande son permis de conduire au candidat, comme il est mineur il n'en a pas, et déclenche donc une erreur. Cette erreur est envoyé comme paramètre à l'instruction catch. img:coupe 2. Provoquer une erreur :arrow: Il est fréquent que dans la méthode d'une de nos classes, on puisse dire au code qui utilise cette classe que quelque chose s'est mal passé. Nous allons donc "diffuser une erreur", au moyen de l'instruction throw. Cette instruction "emet" une erreur qui lui est fournie juste aprés. :arrow: Si une instruction throw n'est pas interceptée par un catch, elle se contente d'afficher l'erreur dans la fenêtre de sortie (comme un trace en fait). :arrow: Voyons comment nous pouvons emmettre une erreur personnalisée lorsqu'un candidat n'est pas majeur :// On crée une fonction qui va vérifier si le candidat est majeur
// S'il ne l'est pas, on emet une erreur avec un message personalisé
function verifierMajorite(c:Individu) {
   if (c.age < 18) {
      throw new Error("Ce candidat n'est pas majeur !");
   }
}
// On déclare notre candidat
var candidat:Individu;
// On commence a surveiller les erreurs
try {
   candidat = new Individu("DURAND","Pierre","18/06/1987");
   verifierMajorite(candidat);
   // La suite de l'entretien
} catch (catchedError : Error) {
   // S'il y a erreur, on affiche le message
   trace(catchedError.message);
}
img:coupe 3. Réagir selon le type d'erreur :arrow: Les instruction try ... catch permettent également de réagir différemment selon le type d'erreur qui survient. Cela est possible grâce aux possibilités de typage fort d'ActionScript 2, en créant plusieurs blocs d'instructions catch qui vont attendre chacun un type d'erreur différent. Cela va nécessiter également de créer des classes qui héritent de la classe Error, et vont posséder leurs propres spécificités.