[VEGAS] [AS3] AndromedAS - le Design Pattern Visitor.
Par eKameleon le dimanche, mai 11 2008, 01:00 - VEGAS - Lien permanent
Introduction
Voici la nouvelle version mise à jour de mon tutoriel sur le Design Pattern Visitor avec cette fois ci un exemple tout en AS3 optimisé et basé sur la toute dernière version de AndromedAS. Ce tutoriel est le premier d'une série de plusieurs autres qui vont illustrer tout doucement toutes les possibilités offertes par l'extension AndromedAS de VEGAS.
Pour ceux qui voudraient relire l'ancienne version de ce tutoriel je vous propose d'aller faire un tour : sur la page de la version AS2 du pattern Visitor
Ce tutoriel remonte à décembre 2006, le temps passe vite ! Depuis de l'eau a coulée sous les ponts et les implémentations du pattern Visitor ont changé de package et se trouvent maintenant dans le package andromeda.util.visitor et plus dans le package vegas.util.* .
A - Généralité
1 - Définition
Le Design Pattern Visitor permet simplement d'ajouter sur une classe ou instance des fonctionnalités non prévues au départ sans surcharger la structure de base de l'objet et en gardant si possible une certaine flexibilité et souplesse pour ajouter ou enlever rapidement d'autres fonctionnalités sans alourdir à nouveau l'implémentation définie au départ.
Je vous conseille de lire en complément de mon explication les articles à ce sujet sur Wikipedia qui vous apporteront peut être quelques explications supplémentaires sur le sujet.
Pour faire simple, le Design Pattern Visitor s'articule autour d'une classe qui pourra être "visitée" par d'autres classes via une méthode accept() (le nom de la fonction peut varier selon les implémentations du pattern). Cette méthode "accept" prend en paramètre un objet de type "visiteur" et va lancer automatiquement la méthode visit() du "visiteur". Ainsi l'objet visiteur pourra contrôler convenablement l'objet visité en récupérant simplement la référence de cet objet.
2 - Implémentation dans AndromedAS
Dans AndromedAS j'ai implémenté 2 interfaces très simples pour structurer ce Design Pattern. Il est bien entendu possible de juste s'inspirer de ces 2 interfaces pour réaliser d'autres conceptualisations selon des besoins précis. Mais dans mes travaux quotidiens ces 2 interfaces sont suffisantes.
2-1 - L'interface andromeda.util.visitor.IVisitable des "visiteurs".
package andromeda.util.visitor { /** * The basic IVisitable interface. * @author eKameleon */ public interface IVisitable { /** * Accept the IVisitor object */ function accept( visitor:IVisitor ):void ; } }
2-2 - L'interface andromeda.util.visitor.IVisitor des objets qui vont être "visités".
package andromeda.util.visitor { /** * The basic IVisitor interface. * To implements the Visitor pattern you can creates a concrete Visitor class who implements this interface. * @author eKameleon */ public interface IVisitor { /** * Visit the IVisitable object. */ function visit( o:IVisitable ):void ; } }
2-3 - La classe abstraite AbstractVisitable
Pour simplifier l'implémentation de ce pattern j'utilise parfois la classe abstraite AbstractVisitable qui permet rapidement d'implémenter ce Design Pattern. Il est bien entendu possible de juste s'inspirer de cette classe pour orienter différemment le Design Pattern selon vos besoins dans vos propres classes qui implémenteront simplement l'interface IVisitable.
package andromeda.util.visitor { import vegas.core.CoreObject; /** * The abstract representation of the IVisitable interface. * To implements a Visitor pattern you must inspired your IVisitor classes with this interface. * This Abstract class is a basical implementation of the Visitor pattern, you can inspirate your custom Visitor design pattern implementation with it easy representation. * @author eKameleon */ public class AbstractVisitable extends CoreObject implements IVisitable { /** * Abstract constructor to creates a concrete constructor when this constructor is extended. */ public function AbstractVisitable() { super(); } /** * Accept a IVisitor object. * You can overrides this method in complexe Visitor pattern implementation. */ public function accept(visitor:IVisitor):void { visitor.visit(this) ; } } }
Toute classe qui hérite de la classe AbstractVisitable va tout simplement récupérer en paramètre via sa méthode accept() des instances de type IVisitor et lancer automatiquement leur méthode visit(). Chaque classe de type IVisitor servira à implémenter une nouvelle fonctionnalité sur la classe IVisitable (notion de plugin).
B - Exemple d'utilisation du Design Pattern (version AS3)
1 - Description et architecture
Nous allons réaliser maintenant un petit exemple simple qui illustrera l'utilisation du Design Pattern Visitor avec la mise en place d'un conteneur graphique qui servira de support pour afficher une image bitmap chargée dynamiquement.
L'exercice est simple, nous allons créer un Sprite qui servira de conteneur pour afficher une image. Nous allons nous servir de la propriété graphics:Graphic de la classe Sprite pour dessiner un fond rectangulaire dans le conteneur (fond de l'image). J'ai choisi de me baser sur la taille de l'image contenue dans l'exemple pour créer un conteneur avec une taille de 260x260 pixels. L'image sera positionnée au centre de la scène principale de l'application.
Dans ce conteneur nous allons ensuite créer un Loader qui permettra à tout moment de charger une image bitmap ou un fichier .swf externe.
Vous pouvez retrouver à tout moment les sources de cet exemple dans les sources AS3 du framework dans le répertoire AS3/trunk/bin/tutorials/andromeda/visitor du repository subversion.
Remarque : Si vous n'avez pas encore installé les sources de VEGAS sur votre machine de travail, je vous conseille vivement de le faire avant de continuer la lecture de cet exemple.
- Installation des sources de VEGAS avec un client SVN : http://code.google.com/p/vegas/wiki/InstallVEGASwithSVN
La structure de ce petit tutoriel reste simple et classique :
1-1 - Répertoire contenant le fichier .fla (pour Flash CS3)
- visitor/bin/visitor.fla : répertoire contenant les sources AS3 du tutoriel.
1-2 - Répertoires contenant le fichier .swf et l'image bitmap externe :
- visitor/deploy/visitor.swf : fichier de production de l'exemple.
- visitor/deploy/library/picture1.jpg : image bitmap chargée par le fichier visitor.swf.
1-3 - Répertoire de base des sources du projet (classpath) :
- visitor/src/ : répertoire contenant les sources AS3 du tutoriel.
- visitor/src/Visitor.as : Classe principale de l'application.
Si vous tester dans Flash CS3 cet exemple il faut ajouter le répertoire src/ de ce petit projet dans les chemins des classes ActionScript :
Edition » Préférence » Paramètres d'ActionScript 3.0 » Ajouter le chemin dans la liste avec le bouton + et ensuite en ciblant le répertoire sur votre disque dur.
1-4 - Répertoire contenant les classes qui permettent de gérer les vues de l'application
- visitor/src/display/
- visitor/src/display/PictureDisplay.as : classe principale de la vue de l'application (conteneur graphique d'une image bitmap).
- visitor/src/display/UIList.as : énumération statique de toutes les vues de l'applications (identifiant unique pour chaque vue importante).
1-5 - Répertoire contenant les IVisitors de la classe PictureDisplay
- visitor/src/visitors/ClearVisitor.as : Ce IVisitor vide l'affichage du conteneur (faire disparaitre l'image uniquement)
- visitor/src/visitors/HideVisitor.as : Ce IVisitor cache complètement le conteneur.
- visitor/src/visitors/LoaderVisitor.as : Ce IVisitor contient les instructions pour charger une image bitmap externe dans le conteneur.
- visitor/src/visitors/ShowVisitor.as : Ce IVisitor affiche complètement le conteneur.
2 - Création de la classe PictureDisplay
Voyons tout d'abord le code source de cette classe :
package visitor.display { import flash.display.*; import flash.events.Event; import andromeda.util.visitor.IVisitable; import andromeda.util.visitor.IVisitor; /** * The PictureDisplay class. */ public class PictureDisplay extends Sprite implements IVisitable { /** * Creates a new PictureDisplay instance. */ public function PictureDisplay() { super() ; loader = new Loader() ; loader.contentLoaderInfo.addEventListener( Event.COMPLETE, complete ) ; update() ; } /** * The virtual height of the picture. */ public var h:uint = 260 ; /** * The loader of the picture display. */ public var loader:Loader ; /** * The virtual hwidth of the picture. */ public var w:uint = 260 ; /** * Accept a IVisitor object. */ public function accept( visitor:IVisitor ):void { visitor.visit(this) ; } /** * Invoked when the picture loading is complete. */ public function complete( e:Event ):void { trace("complete : " + e) ; addChild( loader ) ; update() ; } /** * Update the view of the display. */ public function update():void { graphics.clear() ; graphics.beginFill(0xFFFFFF, 100) ; graphics.drawRect(0, 0, w, h) ; graphics.endFill() ; if ( contains(loader) ) { loader.x = ( w - loader.width ) / 2 ; loader.y = ( h - loader.height ) / 2 ; } } } }
La classe PictureDisplay hérite de la classe AS3 flash.display.Sprite.
Pour les besoins de l'exemple j'ai déclaré 2 attributs w:Number et h:Number qui définissent la largeur et la hauteur utilisées pour dessiner le fond du conteneur. Le conteneur par défaut dessine un fond blanc de 260 par 260 pixels avec la méthode update() qui permet de rafraichir à tout moment la vue du conteneur.
La classe PictureDisplay contient également une attribut de type Loader qui servira de chargeur d'image bitmap externe le moment voulu. J'ai juste connecté cette référence avec son conteneur en abonnant l'objet qui écoute l'évènement Event.COMPLETE qui est notifié à la fin du chargement de l'image si tout se passe bien.
loader = new Loader() ; loader.contentLoaderInfo.addEventListener( Event.COMPLETE, complete ) ;
La méthode "complete" attache juste le loader dans le conteneur si une image est chargée et ensuite remet à jour l'affichage de l'image chargée en la centrant par rapport aux propriétés de la largeur (w) et de la hauteur (h) de l'instance.
3 - Création des visitors de la classe PictureDisplay
Nous mettons en place maintenant 4 types de classes concrètes qui implémentent l'interface IVisitor. Ces classes sont des extensions de classe PictureDisplay, ces visiteurs servent de plugins qui apportent des fonctionnalités supplémentaires sans surcharger la classe principale.
3-1 - La classe ClearVisitor
Voici le contenu de la classe principale de l'application ClearVisitor :
package visitor.visitor { import andromeda.util.visitor.IVisitable; import andromeda.util.visitor.IVisitor; import visitor.display.PictureDisplay; /** * This visitor clear the view of a PictureDisplay instance. * @author eKameleon */ public class ClearVisitor implements IVisitor { /** * Creates a new ClearVisitor instance. */ public function ClearVisitor() { super(); } /** * Clear a PictureDisplay object. * Visit the IVisitable object. */ public function visit( o:IVisitable ):void { var picture:PictureDisplay = o as PictureDisplay ; trace( this + " visit : " + picture ) ; if ( picture != null ) { if ( picture.contains( picture.loader ) ) { picture.removeChild( picture.loader ) ; } } else { throw new Error(this + " 'visit' method failed, the argument of this method must be a PictureDisplay instance.") ; } } } }
Ce visitor est très très simple car il permet juste de vider visuellement l'affichage d'une image chargée. Une référence de la classe ClearVisitor visite une référence de la classe PictureDisplay et essaie de supprimer la référence de son loader dans le conteneur avec la méthode removeChild().
if ( picture.contains( picture.loader ) ) { picture.removeChild( picture.loader ) ; }
la méthode removeChild() désabonne le loader mais ne la supprime pas totalement. Le loader devient donc invisible et sera attaché uniquement si une image est chargée totalement (voir méthode complete() dans la classe PictureDisplay).
3-2 - La classe HideVisitor
Voici le contenu de la classe principale de l'application HideVisitor :
package visitor.visitor { import andromeda.util.visitor.IVisitable; import andromeda.util.visitor.IVisitor; import visitor.display.PictureDisplay; /** * This visitor hide the PictureDisplay reference of the application. */ public class HideVisitor implements IVisitor { /** * Creates a new HideVisitor instance. */ public function HideVisitor() { super(); } /** * Hide a Picture object. * Visit the IVisitable object. */ public function visit(o:IVisitable):void { var picture:PictureDisplay = o as PictureDisplay ; trace( this + " visit : " + picture ) ; if ( picture != null ) { picture.visible = false ; } else { throw new Error(this + " 'visit' method failed, the argument of this method must be a PictureDisplay instance.") ; } } } }
Cette classe visite une référence de la classe PictureDisplay pour la faire disparaitre visiuellement en modifiant juste sa propriété visible.
L'exemple ici peut paraitre un peu trop simple et peu explicite, nous pourrions penser qu'il suffit de lancer directement une instruction simple picture.visible = false dans le code de l'application pour faire disparaitre la référence.
Cet exemple reste très simple car j'évite de le compliquer en ajoutant trop de concepts en même temps, ce qui rendrait peut être plus difficile la compréhension du Design Pattern Visitor...
... Mais je vous propose d'imaginer quelques instants une situation de dernière minute avec une évolution non prévue de l'application avec une nouvelle contrainte technique et graphique : ''L'image doit disparaitre avec un effet de fadding basé sur une Tween sur la propriété numérique alpha, mais aussi avec un léger effet de flou avec la classe flash.filters.BlurFilter"'.
Dans ce cas il sera très simple de créer un nouveau visitor ou de modifier rapidement le visitor existant en ajoutant une transition personnalisée sans avoir encore une fois à modifier la classe PictureDisplay.
Ce visitor nous montre donc qu'il va être possible de créer plusieurs visitors à l'avance ou au fur et à mesure des besoins. Ils proposeront des transitions différentes pour faire disparaitre une image dans notre application. Le code devient donc modulaire et réutilisable. Dans nos prochaines applications nous pourrons utiliser ces visitors sans soucis sans avoir à reprendre complètement tout le code.
3-3 La classe ShowVisitor
Voici le contenu de la classe principale de l'application "ShowVisitor" :
package visitor.visitor { import andromeda.util.visitor.IVisitable; import andromeda.util.visitor.IVisitor; import visitor.display.PictureDisplay; /** * This visitor show the PictureDisplay reference of the application. */ public class ShowVisitor implements IVisitor { /** * Creates a new ShowVisitor instance. */ public function ShowVisitor() { super(); } /** * Clear a PictureDisplay object. * Visit the IVisitable object. */ public function visit(o:IVisitable):void { var picture:PictureDisplay = o as PictureDisplay ; trace( this + " visit : " + picture ) ; if ( picture != null ) { picture.visible = true ; } else { throw new Error(this + " 'visit' method failed, the argument of this method must be a PictureDisplay instance.") ; } } } }
Identique à la classe HideVisitor, cette classe visite un objet de type PictureDisplay et affiche celui ci en donnant une valeur true à la propriété visible de l'instance.
Je ne vais pas reprendre l'explication de la classe HideVisitor pour ce nouveau visitor car elle est en tout point identique mais je propose à tous ceux qui sont un peu curieux de créer votre propre visitor basé sur celui ci est d'en profiter pour cabler à l'intérieur une transition un peu plus complexe en utilisant par exemple la classe pegas.transitions.Tween de l'extension PEGAS de VEGAS.
Rien de mieux qu'un peu de travail pratique pour comprendre complètement un concept.
3-4 - La classe LoaderVisitor
Cette classe est la plus importante dans notre exemple, c'est elle qui ajoute la fonctionnalité qui permet à la classe Picture de charger une image externe.
Voici le contenu de la classe principale de l'application "LoaderVisitor" :
package visitor.visitor { import flash.display.Loader; import flash.net.URLRequest; import andromeda.util.visitor.IVisitable; import andromeda.util.visitor.IVisitor; import visitor.display.PictureDisplay; public class LoaderVisitor implements IVisitor { /** * Creates a new LoaderVisitor instance. * @param url The String representation of the full qualified file name to load. */ public function LoaderVisitor( url:String=null ) { request = new URLRequest(url) ; } /** * The URLRequest of the visitor. */ public var request:URLRequest ; /** * Indicates the url of this LoaderVisitor. */ public function get url():String { return request.url ; } /** * @private */ public function set url(sUrl:String):void { request.url = sUrl ; } /** * Loader a Picture object. * Visit the IVisitable object. */ public function visit( o:IVisitable ):void { var picture:PictureDisplay = o as PictureDisplay ; trace( this + " visit : " + picture ) ; if ( picture != null ) { picture.accept( new ClearVisitor() ) ; picture.loader.load( request ) ; } else { throw new Error(this + " 'visit' method failed, the argument of this method must be a PictureDisplay instance.") ; } } } }
Dans cette classe j'utilise le visitor très simplement en récupérant dans la méthode visit() la référence de la classe PictureDisplay et son loader nécessaire pour charger l'image externe.
4 - Code principal de l'application
Voici le contenu de la classe principale de l'application "Visitor" :
package
{
import flash.display.*;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
import visitor.display.PictureDisplay;
import visitor.visitor.HideVisitor;
import visitor.visitor.LoaderVisitor;
import visitor.visitor.ShowVisitor;
/**
* The main class of the visitor tutorial.
* @author eKameleon
*/
public class Visitor extends Sprite
{
/**
* Creates a new visitor instance.
*/
public function Visitor()
{
// stage
stage.scaleMode = StageScaleMode.NO_SCALE ;
stage.align = StageAlign.TOP_LEFT ;
stage.addEventListener(KeyboardEvent.KEY_DOWN , keyDownHandler ) ;
// view
picture = new PictureDisplay() ;
picture.x = 100 ;
picture.y = 100 ;
addChild( picture ) ;
// test
picture.accept( new LoaderVisitor( "library/picture1.jpg" )) ;
}
/**
* The picture display reference.
*/
public var picture:PictureDisplay ;
/**
* Invoked when a key is down.
*/
private function keyDownHandler( e:KeyboardEvent = null ):void
{
var code:uint = e.keyCode ;
trace("key down : " + code) ;
switch( code )
{
case Keyboard.UP :
{
picture.accept( new HideVisitor() ) ;
break ;
}
case Keyboard.DOWN :
{
picture.accept( new ShowVisitor() ) ;
break ;
}
}
}
}
}
Dans cette classe le code est très simple :
- En premier j'initialise les paramètres globaux de la scène principale (alignement, etc.).
- Puis j'initialise la vue avec une instance de la classe PictureDisplay.
- Enfin je défini l'url de l'image externe que je souhaite charger.
- Pour finir nous pouvons tester les visitors HideVisitor et ShowVisitor en appuyant sur les touches UP et DOWN du clavier.
Dans Flash CS3 il suffit d'ajouter cette classe dans le panneau de propriété du document de votre fichier .fla pour tester l'exemple (voir fichier visitor.fla).
Conclusion
Je pense que ce petit refactoring du tutoriel original apporte un bon coup de jeune.
Je vous rappelle comme toujours pour ceux qui découvrent ici mon framework et ses implémentations que vous pouvez retrouver sur le wiki du Google Code de VEGAS le tutoriel complet sur comment installer les sources et exemples du projet : Install the VEGAS project with Subversion and TortoiseSVN.
Pour ceux qui ont des questions ou qui veulent suivre l'actualité de VEGAS et ses extensions vous pouvez vous abonner sur le Google Groups : VEGASoS
Je suis activement en cours de refactoring AS3 des tutoriels sur les Design Pattern MVC basés sur le DP Observer ou sur un FrontController. Ensuite je peux vous annoncer dans très peu de temps un tout nouveau tutoriel (en plusieurs parties ?) Sur mon implémentation du Design Pattern d'Inversion de contrôle (IoC) avec AndromedAS et un DOM au format eden.
Pour les plus impatients d'entre vous vous pouvez déjà regarder les sources de ces tutoriels dans la branche AS3 de VEGAS et pour les plus motivés d'entre vous... vous pouvre déjà consulter une ébauche de mon futur tutoriel IoC sur le wiki de AndromedAS : http://code.google.com/p/andromed-as/wiki/TutorialsAndromedA_IOC (en anglais pour le moment mais en cours d'écriture en français)
Je me rends compte que vu le nombre de tutoriels en cours et qui vont arriver il y a de forte chance que tout cela termine en bouquin opensource en complément du framework disponible en ligne sur Google Documents avec pleins d'autres surprises cet été 
En attendant bonne lecture 
PS : désolé pour les petites perturbations sur le blog ce week end, j'ai eu des problèmes de changement d'hébergement chez Amen et ensuite problème pour migrer mon blog sur DotClear2 mais maintenant tout semble ok. N'hésitez pas à me dire si vous trouvez des bugs sur mon blog 
Commentaires
Bonjour, je me permets de vous contacter via les comentaires de blog, toutes mes excuses par avance !
Le studio grouek, www.grouek.com, fondé en 2008, basé à Paris, FWA Juin 2008, memebre du réseau créatif TYO International, recherche des développeurs Flash AS3. Si vous êtes intéressé ou connaissez des personnes intéressées, n'hésitez pas à me contacter : sacco@grouek.com
Merci
Cordialement.
C'est une idee de moi ou c'est un peu superflu d'utiliser toutes ces interfaces. On dirait du copier coller de design pattern en Java pas de l'actionscript..
C'est beaucoup plus simple avec une Closure.
Ma version:
// Un objet quelconque
public class Thing
{
var _value:Number;
public get value():Number{ return _value; }
}
// Un container d'objet quelconque....
public class Container
{
var listOfThings:Array = new Array();
public function visit(visitor:Function)
{
for each(t:Thing in listOfThings)
visitor(t);
}
}
-------------------
Usage:
On imagine que Container a ete instancie precedement et que la liste est rempli de "Thing".
On "visit" chaque Thing et on calcule la somme de leur valeur.
Beaucoup plus simple avec un "Closure" qu'avec des interface partout qui n'avance a rien car il faut de toutes facon faire un typecast...
-------------------
public function blabla()
{
....
var sum:Number = 0;
myContainer.visit(
function(thing:Thing)
{
sum += thing.value;
}
);
trace("the sum is:" + sum);
....
}
---------
Si on veut ajouter un niveau d'indirection on peut rajouter une fonction "visit" a l'objet Thing mais a part rajouter un appel de fonction je ne vois pas l'interet dans la plupart des cas.
Hello
Et tu fais comment ensuite pour spécialiser ton visitor et faire en sorte que l'objet passé dans la méthode visite soit compatible uniquement avec l'objet visité ?
Je rappel que l''exemple au dessus est basique mais tout de même faut bien garder à l'idée la force des interfaces
Si tu as par exemple une classe "Car" tu pourras créer une interface avec des "CarVisitor" par exemple... Et seuls les objets qui implémentent cette interface (avec peut être d'autres méthodes et propriétés définies à l'intérieur) pourront être compatible avec la classe "Car"
A noter sinon que sans l'interface tu ne peux pas t'assurer que la fonction qui va être envoyée à ta méthode sera bien du type de l'objet qu'il visite
Donc il est clair que l'on peut faire autrement.. mais sans l'interface tu perds totalement au niveau de la sécurité du typage et l'interface permet tout de même d'avoir une forte souplesse par la suite que ta fonction ne te permets pas directement.
Maintenant je le répète on peut interpréter un Design Pattern comme on le sent... mais quand tu dis que cela fait "JAVA" au dessus... je ne suis pas d'accord justement
Les interfaces font parti du langage AS3 faut apprendre à s'en servir 
PS : Il y a de forte chance que mes interfaces changent de noms d'ici peu de temps avec un nom plus simple Visitor et Visitable tout simplement.
PS2 : Mon implémentation ici est super simple et il faut surtout s'en servir comme référence pour créer vos propres implémentations.
EKA+