Utilisation du Design Pattern Visiteur avec le framework VEGAS

Marc Alcaraz

Premier tutoriel d'une série destinée à montrer comment utiliser différents motifs de conception à partir du framework Vegas.

Nous commençons cette série par le Design Pattern Visiteur.

- Titre : Utilisation du Design Pattern Visiteur avec le framework VEGAS

- Auteur : Marc Alcaraz

- Parution : 4 juin 2007

- Mise à jour : 13 juin 2020

- Licence : Copyright ® 2006-2007 Marc Alcaraz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Généralité

- III. Exemple d'utilisation du pattern

- IV. Conclusion

I - Introduction

Cela fait quelque temps maintenant que je vous parle de VEGAS, mais il est vrai qu'il manquait des vrais tutoriels pour vous montrer comment utiliser mon framework…

Je trouve intéressante l'idée de progressivement vous amener à conceptualiser l'intégralité de vos applications avec lui. Je vais en profiter au passage pour mettre en avant certains outils précis en passant en revue quelques Design Patterns (FrontController, Observer, Singleton, Visitor…). Il est certain que, dans ma façon de travailler, certains vont peut-être reconnaitre des notions déjà vues ailleurs dans d'autres frameworks comme Pixlib par exemple… Pas de secret, les quelques projets que j'ai pu réaliser cette année avec Francis Bourre ont étaient décisifs dans l'orientation actuelle de mon travail au niveau de l'utilisation d'un FrontController pour globaliser les flux d'événements dans mes applications.

Je vais donc commencer ces tutoriels par la mise en avant de quelques Design Patterns que j'aime bien utiliser au quotidien dans mes applications ActionScript.

Voici donc un premier tutoriel basé sur le pattern Visitor. À mon avis, ce pattern n'est pas très connu dans le domaine de l'ActionScript, je ne l'ai jamais rencontré et j'ai eu un peu de mal à trouver une implémentation simple pour l'utiliser simplement en ActionScript… Donc j'espère que les experts en Design Patterns pourront me dire si ma vision de ce pattern ne leur parait pas trop éloignée de sa vraie nature. L'exemple présenté ici est bien entendu qu'un petit exercice sans prétention pour comprendre les concepts de base.

II - Généralité

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. En complément de mon explication, vous pouvez lire cet article à ce sujet.

Pour faire simple, le 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 controler convenablement l'objet « visité » en récupérant simplement la référence de cet objet.

Dans VEGAS, j'ai implémenté deux interfaces très simples pour structurer ce Pattern. Il est bien entendu possible de juste s'inspirer de ces deux interfaces pour réaliser d'autres conceptualisations selon des besoins précis. Mais dans mes travaux quotidiens, ces deux interfaces sont bien suffisantes.

Voyons de plus près, ces deux interfaces dans la librairie AS2 de VEGAS :

1 - vegas.util.visitor.IVisitable : l'interface des « visiteurs ».

import vegas.util.visitor.IVisitor;

/**
* The basic IVisitor interface.
* @author eKameleon
*/
interface vegas.util.visitor.IVisitable
{

/**
* Accept a IVisitor object
*/
function accept( visitor:IVisitor ) ;

}

2 - vegas.util.visitor.IVisitor : l'interface des objets qui vont être « visités ».

import vegas.util.visitor.IVisitable;

/**
* The basic IVisitor interface.
* To implements the Visitor pattern you can creates a concrete Visitor class who implements this interface.
* @author eKameleon
*/
interface vegas.util.visitor.IVisitor
{

/**
* Visit the IVisitable object.
*/
function visit( o:IVisitable ):Void ;

}

Je conceptualise très simplement ce pattern pour le moment et pour cela j'utilise simplement une classe abstraite AbstractVisitable qui me permet rapidement d'implémenter ce Pattern. Il est bien entendu possible de juste s'inspirer de cette classe pour orienter différemment le DP selon vos besoins dans vos propres classes qui implémenteront simple l'interface IVisitable.

import vegas.core.CoreObject;
import vegas.util.visitor.IVisitable;
import vegas.util.visitor.IVisitor;

/**
* 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
*/
class vegas.util.visitor.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.
*/
function accept( visitor:IVisitor )
{
visitor.visit(this) ;
}

}

Vous pouvez voir, comme je l'ai expliqué plus haut, qu'une 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.

III - Exemple d'utilisation du pattern

Pour illustrer simplement ce Design Pattern j'ai décidé de mettre en avant une classe Picture qui sera IVisitable et qui va me permettre de gérer la vue d'une image chargée dynamiquement dans un clip. L'exercice de ce tutoriel servira de bases pour tous les autres tutoriels qui vont arriver donc je vais essayer d'être précis dans mes explications.

Remarque : Vous pouvez retrouver les sources de cet exemple dans le subversion de VEGAS dans le répertoire AS2/trunk/bin/test/vegas/util/visitor/

Au niveau visuel l'exercice nécessite simplement un clip contenant une simple forme rectangulaire. J'ai choisi de créer un rectangle de 260 pixels par 260 (je me base avec cette taille sur les images qui seront chargées dans le clip via un clip « container » dynamique). Je pose au centre de la scène de mon application ce clip et je lui donne comme nom d'occurrence : picture_mc.

À mon avis à ce niveau-là pas trop de difficulté ;) Vous pouvez retrouver le fla principal de cet exercice avec le fichier Picture.fla dans le répertoire défini au-dessus.

Toutes les classes de l'exemple sont dans le répertoire test/ à la racine du répertoire contenant le fichier Picture.fla.

1 - Je commence simplement par la classe test.visitor.diaporama.Picture qui hérite de AbstractVisitable.

import vegas.util.visitor.AbstractVisitable;
import vegas.util.visitor.IVisitor;

/**
* The Picture class to display a picture in a Movieclip who contains a container movieclip inside.
* @author eKameleon
*/
class test.visitor.diaporama.Picture extends AbstractVisitable
{

/**
* Creates a new Picture instance.
*/
public function Picture( name:String, target:MovieClip , url:String , hide:Boolean)
{

this.name = name ;

this.url  = url ;

view = target ;

container = view.createEmptyMovieClip("container", 1) ;
container._x = margin ;
container._y = margin ;

setVisible( !hide ) ;
}

/**
* The container reference in the picture display.
*/
public var container:MovieClip ;

/**
* The margin in the picture to display the container.
*/
public var margin:Number = 10 ;

/**
* The name of the picture.
*/
public var name:String ;

/**
* The url of the picture.
*/
public var url:String ;

/**
* The view of the picture.
*/
public var view:MovieClip ;

/**
* Returns true if the picture is visible.
*/
public function isVisible():Boolean
{
return view._visible ;
}

/**
* Sets the visible property of the picture.
*/
public function setVisible( b:Boolean ):Void
{
view._visible = b ;
}

/**
* Returns the string representation of this object.
* @return the string representation of this object.
*/
public function toString():String
{
return "[Picture " + name + "]" ;
}

}

Cette classe est très simple, elle utilise un traitement par composition de la vue définie en paramètre de la fonction constructeur. Je vais détailler les points importants de cette classe :

Héritage de la classe AbstractVisitable qui assure l'implémentation de l'interface IVisitable

/**
* The Picture class to display a picture in a Movieclip who contains a container movieclip inside.
* @author eKameleon
*/
class test.visitor.diaporama.Picture extends AbstractVisitable

Fonction constructeur de la classe qui prend en paramètre la cible du clip qui servira de conteneur pour l'image chargée dynamiquement :

/**
* Creates a new Picture instance.
*/
public function Picture( name:String, target:MovieClip , url:String , hide:Boolean)
{

this.name = name ;

this.url  = url ;

view = target ;

container = view.createEmptyMovieClip("container", 1) ;
container._x = margin ;
container._y = margin ;

setVisible( !hide ) ;
}

J'ai défini une propriété name qui permet de définir un nom à l'image à afficher, une propriété url de type String qui permet de définir l'url de l'image externe et un paramètre booléen hide qui permet de définir si le clip contenant l'image est visible par défaut lorsque l'on instancie pour la première fois cette classe.

Je profite du constructeur de la classe Picture pour créer dynamiquement dans le movieClip passé en paramètre un clip vide « container » qui servira de conteneur principal pour charger l'image par la suite.

Les 2 seules méthodes importantes de cette classe sont isVisible():Boolean et setVisible(b:Boolean).

Si vous prenez le temps de lire le contenu de cette classe à mon avis vous n’aurez aucun problème pour comprendre son utilisation. Il ne faut pas oublier l'existance de la méthode accept() définie dans la classe abstraite AbstractVisitable (c'est tout de même cette méthode qui compte le plus finalement pour utiliser le DP Visitor :D).

Pour tester cette classe, il vous suffit dans le fla qui contient votre clip qui servira de base pour charger une image de taper le code suivant :

import test.visitor.diaporama.Picture ;

var url:String = "library/picture1.jpg" ;
var picture:Picture = new Picture("picture1", picture_mc, url) ;

Pour le moment il ne se passe pas grand-chose, c'est normal nous n'avons pas encore mis en place les IVisitor de cette classe qui vont lui permettre d'étendre ses fonctionnalités.

2 - Mise en place des IVisitor de la classe Picture. Les IVisitor sont des extensions de la classe qui pour chaque classe implémente une fonctionnalité précise liée à la classe principale.

La classe 'ClearVisitor' : permet de vider simplement le contenu d'une instance de la classe Picture.

import test.visitor.diaporama.Picture;

import vegas.core.CoreObject;
import vegas.errors.IllegalArgumentError;
import vegas.util.visitor.IVisitable;
import vegas.util.visitor.IVisitor;

/**
* This class clear the view of a Picture instance.
* @author eKameleon
*/
class test.visitor.diaporama.visitors.ClearVisitor extends CoreObject implements IVisitor
{

/**
* Creates a new ClearVisitor instance.
*/
public function ClearVisitor()
{
super();
}

/**
* Clear a Picture object. Visit the IVisitable object.
*/
public function visit( o:IVisitable ):Void
{
if (o instanceof Picture)
{
trace("> " + this + " visit " + o) ;
var view = Picture(o).view ;
if (view.container.picture != null)
{
view.container.picture.removeMovieClip() ;
}
}
else
{
throw new IllegalArgumentError(this + " 'visit' method failed, the argument of this method must be a Picture instance.") ;
}
}
}

Il est donc important de noter dans cette classe l'implémentation de l'interface « IVisitor » et la mise en place de la méthode visit() qui prend en paramètre un object « IVisitable ». La méthode visit() teste si l'instance passée en paramètre est bien un objet de type Picture et si c'est le cas il vide son contenu. À noter que j'utilise dans cette méthode les erreurs définies dans VEGAS dans le package vegas.errors.*. Je reviendrais dans un autre tutoriel sur la gestion des erreurs.

Toutes mes classes de base héritent de la classe vegas.core.CoreObject qui implémente quelques fonctionnalités de base importantes liées à VEGAS.

La classe 'HideVisitor' : permet simplement de lancer la fonction setVisible(b:Boolean) d'une instance de la classe Picture avec une valeur false (cacher l'image)

import test.visitor.diaporama.Picture;

import vegas.core.CoreObject;
import vegas.errors.IllegalArgumentError;
import vegas.util.visitor.IVisitable;
import vegas.util.visitor.IVisitor;

/**
* This class hide the view of a Picture instance.
* @author eKameleon
*/
class test.visitor.diaporama.visitors.HideVisitor extends CoreObject 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
{
if (o instanceof Picture)
{
trace("> " + this + " visit " + o) ;
Picture(o).setVisible( false ) ;
}
else
{
throw new IllegalArgumentError(this + " 'visit' method failed, the argument of this method must be a Picture instance.") ;
}
}

}

La classe 'ShowVisitor' : permet simplement de lancer la fonction setVisible(b:Boolean) d'une instance de la classe Picture avec une valeur true (afficher l'image)

import test.visitor.diaporama.Picture;

import vegas.core.CoreObject;
import vegas.errors.IllegalArgumentError;
import vegas.util.visitor.IVisitable;
import vegas.util.visitor.IVisitor;

/**
* This class release the view and the properties of a Picture instance.
* @author eKameleon
*/
class test.visitor.diaporama.visitors.ReleaseVisitor extends CoreObject implements IVisitor
{

/**
* Creates a new ReleaseVisitor instance.
*/
public function ReleaseVisitor()
{
super();
}

/**
* Release a Picture object. Visit the IVisitable object.
* Initialize the Picture and remove the target reference.
*/
public function visit( o:IVisitable ):Void
{
if (o instanceof Picture)
{
trace("> " + this + " visit " + o) ;
Picture(o).view.removeMovieClip() ;
Picture(o).url = null ;
Picture(o).name = null ;
}
else
{
throw new IllegalArgumentError(this + " 'visit' method failed, the argument of this method must be a Picture instance.") ;
}
}

}

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.

import test.visitor.diaporama.Picture;
import test.visitor.diaporama.visitors.ClearVisitor;
import test.visitor.diaporama.visitors.ShowVisitor;

import vegas.core.CoreObject;
import vegas.errors.IllegalArgumentError;
import vegas.errors.Warning;
import vegas.events.Delegate;
import vegas.util.visitor.IVisitable;
import vegas.util.visitor.IVisitor;

/**
* This class load an external picture in a Picture instance.
* @author eKameleon
*/
class test.visitor.diaporama.visitors.LoaderVisitor extends CoreObject implements IVisitor
{

/**
* Creates a new LoaderVisitor instance.
*/
public function LoaderVisitor( bAutoShow:Boolean )
{

super();

autoShow = bAutoShow ;

}

/**
* Defined if the picture is show when is loading.
*/
public var autoShow:Boolean ;

/**
* Load a Picture object with this current url. Visit the IVisitable object.
*/
public function visit( o:IVisitable ):Void
{
if (o instanceof Picture)
{
trace("> " + this + " visit " + o) ;
var view = Picture(o).view ;
var url:String = Picture(o).url ;

// clear the picture.
Picture(o).accept( new ClearVisitor() ) ;

if (url.length > 0)
{

var listener:Object = {} ;
listener.onLoadInit = Delegate.create(this, onLoadInit, Picture(o) ) ;

var target:MovieClip = view.container.createEmptyMovieClip("picture", 1) ;
var loader:MovieClipLoader = new MovieClipLoader() ;
loader.addListener(listener) ;

loader.loadClip(url , target) ;

}
else
{
throw new Warning(this + " the picture '" + o + "' can't be loading with an empty url property.") ;
}

}
else
{
throw new IllegalArgumentError(this + " 'visit' method failed, the argument of this method must be a Picture instance.") ;
}
}

/**
* Invoqued when the loading of a Picture is finished and initialized.
*/
public function onLoadInit( target:MovieClip , picture:Picture ):Void
{

trace("> " + this + " picture " + picture + " is loading and init.") ;

if ( autoShow == true )
{
picture.accept( new ShowVisitor() ) ;
}

}
}

J'utilise dans l'exemple la classe MovieClipLoader pour charger l'image définie par la propriété url définie dans l'instance de type Picture. Il est intéressant de voir qu'il est possible de définir des paramètres dans la fonction constructeur d'un objet qui implémenter l'interface IVisitor. Vous pouvez pour fini regarder le contenu de la méthode visit() de cette classe, j'utilise un appel direct à la fonction HideVisitor dans cette méthode pour cacher l'image au lancement du chargement. Dans la méthode onLoadInit la classe ShowVisitor est utilisée pour afficher à nouveau l'image une fois chargée (si autoShow == true bien entendu).

Comme vous pouvez le voir, l'intérêt de ces classes est d'isoler toutes les fonctionnalités de la classe Picture sans surcharger celle-ci avec des méthodes inutiles. Je considère que le pattern Visitor est un bon compromis pour créer des plugins sur mes classes. À moi ensuite d'installer ou non les plugins selon mes besoins dans les différentes applications que je mettrais en place par la suite.

3 - Pour finir, voyons le code principal de l'application qui se trouve dans le Picture.fla :

/**
* Design Pattern Visitor with VEGAS.
* eKameleon - 2006
*/

import test.visitor.diaporama.visitors.* ;
import test.visitor.diaporama.Picture ;

var url:String = "library/picture1.jpg" ;
var picture:Picture = new Picture("picture1", picture_mc, url) ;

// Hide the picture
picture.accept(new HideVisitor()) ;

// load the picture by default and auto show when is loading.
picture.accept(new LoaderVisitor(true)) ;

this.onKeyDown = function()
{
var code:Number = Key.getCode() ;
switch(code)
{
case Key.UP :
{
picture.accept(new HideVisitor()) ;
break ;
}
case Key.DOWN :
{
picture.accept(new ShowVisitor()) ;
break ;
}
case Key.LEFT :
{
picture.accept(new ClearVisitor()) ;
break ;
}
case Key.RIGHT :
{
picture.url = "library/picture2.jpg" ;
picture.accept(new LoaderVisitor()) ;
break ;
}
}
}
Key.addListener(this) ;

trace("> Press Key.UP to hide the picture.") ;
trace("> Press Key.DOWN to show the picture.") ;
trace("> Press Key.LEFT to clear the picture.") ;
trace("> Press Key.RIGHT to load the picture.") ;

Vous voyez qu'il devient très simple à tout moment d'exécuter une commande externe de type IVisitor sur l'instance de la classe Picture qui est IVisitable.

IV - Conclusion

Reste maintenant à créer un petit diaporama pour afficher dans notre « vue » plusieurs images. Je vais attendre les 2 prochains tutoriels pour vous montrer comment il est possible d'utiliser le Design Pattern Observer et ensuite le modèle événementiel de VEGAS (encore mieux) pour gérer ce type d'application via un MVC propre et efficace.

Je vais pour le moment vous laisser le temps de lire correctement ce tutoriel et d'essayer à votre tour d'implémenter ce pattern avant de passer à la suite.

Pour ceux qui découvrent ce tutoriel et VEGAS du même coup, pour installer mon framework vous pouvez lire mon article à ce sujet : Installation des sources via Subversion.