Utilisation des motifs de conception MVC et Observateur avec le framework VEGAS

Marc Alcaraz

Suite de la série consacrée à l'utilisation des design patterns sous VEGAS.
Au programme : les designs patterns MVC et Observateur.

- Titre : Utilisation des motifs de conception MVC et Observateur avec le framework VEGAS

- Auteur : Marc Alcaraz

- Parution : 11 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. Implémentation dans VEGAS

- IV. Exemple d'utilisation de ces deux patterns

I - Introduction

J'ai décidé de prolonger le précédent tutoriel sur le Design Pattern Visitor en reprenant la base de ce dernier et en ajoutant cette fois-ci une implémentation simple du Design Pattern MVC (Model View Controller) en utilisant le pattern Observer.

Je vous avoue que je n'utilise pratiquement jamais cette implémentation, préférant une centralisation du pattern MVC via un FrontController et le modèle événementiel de VEGAS basé sur le DOM level 2/3 du modèle événementiel standardisé par le W3C. Je vous présenterai dans le prochain tutoriel ce type d'implémentation qui reste pour moi la meilleure implémentation du MVC avec VEGAS.

Mais alors pourquoi vous parler du pattern Observer me direz vous ?

Tout simplement, car le pattern Observer est très connu et qu'il peut être intéressant de voir comment l'utiliser avec VEGAS. Je pense également à ceux qui débutent et qui ne le connaissent pas du tout… cela peut vous aider à mieux comprendre son implémentation. C'est ce qu'on appelle faire d'une pierre deux coups ! :D

II - Généralité

Commençons par une petite explication théorique et très simple sur les patterns MVC et Observer.

L'architecture Modèle Vue Contrôleur ou MVC est un modèle de conception très utilisé de nos jours dans de nombreux domaines et langages. Son objectif est de séparer la vue d'une application de ses modèles de données, un contrôleur permet le lien entre la vue du modèle. Je ne vais pas aller beaucoup plus loin dans l'explication de ce pattern, vous trouverez plus d'informations dans cet article. L'exemple qui va suivre servira de support à cette notion et si vous désirez que dans un prochain tutoriel je rentre plus en profondeur dans mes explications, n'hésitez pas à me le faire savoir.

Le Design Pattern Observer (ou Observateur en français) est un simple modèle qui permet à un objet (Observable) d'envoyer des événements et des informations vers d'autres objets (les Observateurs ou « Observer »). Les objets Observer vont donc exécuter certaines actions en fonction des indications envoyées par l'objet Observable.

En suivant cette idée d'un objet qui émet des événements en cas de changement d'état. Il est très intéressant de se pencher sur le pattern MVC. En effet, la grande force du pattern MVC est de permettre au modèle d'émettre des événements vers le contrôleur en cas de changement d'état et ainsi de permettre à l'application de modifier comme il faut la vue en fonction des nouvelles informations reçues.

En ActionScript, il est tout à fait possible de considérer la classe native AsBroadcaster comme une classe utilitaire capable de générer des objets Observables qui émettront des événements vers des objets Observer.

III - Implémentation dans VEGAS

Dans VEGAS, je n'utilise pas la classe AsBroadcaster pour implémenter le pattern Observer. Je m'inspire tout simplement de l'implémentation de ce pattern en JAVA (en adaptant un peu tout de même) en utilisant une interface IObserver et une classe Observable.

L'interface IObserver permet d'implémenter toutes les classes qui permettront de créer des objets capables d'intercepter des modifications effectuées sur un objet Observable. Cette interface très simple a juste besoin d'une méthode update() qui récupère en paramètre une référence vers l'objet observé et un argument facultatif définissant l'information ou l'événement émis par l'Observer en cas de changement de celui-ci.

import vegas.util.observer.Observable ;

/**
* A class can implement the Observer interface when it wants to be informed of changes in observable objects.
* @author eKameleon
*/
interface vegas.util.observer.IObserver
{

/**
* This method is called whenever the observed object is changed.
* @param o the observable object.
* @param arg an argument passed to the notifyObservers method.
*/
function update(o:Observable, arg) ;

}

La classe Observable est un peu plus complexe que l'interface IObserver. Regardons tout d'abord cette classe de plus près avant quelques explications à son sujet.

import vegas.core.CoreObject;
import vegas.data.iterator.Iterator;
import vegas.data.list.ArrayList;
import vegas.errors.NullPointerError;
import vegas.util.observer.IObserver;

/**
* This class represents an observable object, or "data" in the model-view paradigm.
* It can be subclassed to represent an object that the application wants to have observed.
* @author eKameleon
*/
class vegas.util.observer.Observable extends CoreObject
{

/**
* Creates an Observable instance with zero Observers.
*/
public function Observable()
{
_obs = new ArrayList() ;
}

/**
*  Adds an observer to the set of observers for this object,
* provided that it is not the same as some observer already in the set.
*/
public function addObserver(o:IObserver):Boolean
{
if (o == null)
{
throw new NullPointerError(this + " the passed object in argument not must be 'null' or 'undefined'.") ;
}
if (!_obs.contains(o))
{
_obs.insert(o) ;
return true ;
}
return false ;
}

/**
* Indicates that this object has no longer changed,
* or that it has already notified all of its observers of its most recent change,
* so that the hasChanged method will now return false.
*/
public function clearChanged():Void
{
_changed = false ;
}

/**
* Tests if this object has changed.
*/
public function hasChanged():Boolean
{
return _changed ;
}

/**
* If this object has changed, as indicated by the hasChanged method,
* then notify all of its observers and then call the clearChanged method to indicate that this object has no longer changed.
*/
public function notifyObservers( arg ):Void
{
if (arg == undefined)
{
arg = null ;
}
if (!_changed)
{
return ;
}
clearChanged() ;
var _obsMemory:ArrayList = _obs.clone() ;
var it:Iterator = _obsMemory.iterator() ;
while(it.hasNext())
{
IObserver(it.next()).update(this, arg) ;
}
}

/**
* Removes an observer from the set of observers of this object.
*/
public function removeObservers(o:IObserver):Void
{
if (o == undefined)
{
_obs.clear() ;
}
else
{
_obs.remove(o) ;
}
}

/**
* Marks this Observable object as having been changed; the hasChanged method will now return true
*/
public function setChanged():Void
{
_changed = true ;
}

/**
* Returns the number of observers of this Observable object.
* @return the number of observers of this Observable object.
*/
public function size():Number
{
return _obs.size() ;
}

/**
* The internal ArrayList.
*/
private var _obs:ArrayList ;

/**
* The internal value to notify if this Observable object is changed or not.
*/
private var _changed:Boolean ;

}

Cette classe utilise un ADT (abstract data type) de type ArrayList qui permet de gérer la liste des IObserver enregistrés avec la méthode addObserver(). Une fois un objet de type IObserver inséré dans cette liste, il recevra dans sa fonction update() toutes les émissions d'événements ou toutes les informations propulsées par la méthode notifyObservers() de la classe. La méthode removeObserver() permet de supprimer un IObserver enregistré dans la ArrayList de l'instance. Il reste la méthode size() qui est une méthode qui informe simplement sur le nombre d'IObserver enregistrés.

Les présentations sont maintenant faites… passons aux choses sérieuses.

IV - Exemple d'utilisation de ces deux patterns

Comme je vous l'ai dit au début de ce tutoriel, je vais reprendre la base du tutoriel traitant du Pattern Visitor. Je vous conseille donc de le relire et si possible de tester l'exemple fourni dans les sources de VEGAS dans le répertoire AS2/trunk/bin/test/vegas/util/validator

Pour cet exercice, j'ai donc juste changé le nom du package en test.observer et je réutilise les classes Picture (la Vue de l'application) et ses Visitors : ClearVisitor, HideVisitor, LoaderVisitor, ReleaseVisitor et ShowVisitor.

L'exemple se base sur un mini diaporama d'images avec un modèle de données très simple qui permet de stocker les URL de plusieurs images externes que l'on va charger dynamiquement dans notre display « Picture » se trouvant sur la scène principale d'un fla.

J'utilise donc une classe PictureModel qui sera mon objet Observable et en même temps le modèle de mon application.

import test.observer.diaporama.events.PictureModelEvent;

import vegas.core.IRunnable;
import vegas.data.iterator.Iterator;
import vegas.data.Set;
import vegas.data.set.HashSet;
import vegas.util.observer.Observable;

/**
* The model to change the Picture with differents external files.
* @author eKameleon
*/
class test.observer.diaporama.model.PictureModel extends Observable implements IRunnable
{

/**
* Creates a new PictureModel.
*/
public function PictureModel()
{
super();

_eClear   = new PictureModelEvent( PictureModelEvent.CLEAR) ;
_eLoad = new PictureModelEvent( PictureModelEvent.LOAD) ;
_eVisible = new PictureModelEvent( PictureModelEvent.VISIBLE ) ;
_set = new HashSet() ;
_it = _set.iterator() ;
}

/**
* Clears the model.
*/
public function clear():Void
{
_set.clear() ;
_it = _set.iterator() ;
notifyChanged( _eClear );
}

/**
* Returns the string representation of the current picture url.
*/
public function getUrl():String
{
return _eLoad.getUrl() ;
}

/**
* Hide the picture.
*/
public function hide():Void
{
_eVisible.setVisible(false);
notifyChanged(_eVisible) ;
}

/**
* Inserts a new picture's url in the model, if the url exist the url isn't inserted.
* @return 'true' if the url is inserted else 'false' if the url allready exist in the model.
*/
public function insertUrl( sUrl:String ):Boolean
{
return _set.insert( sUrl ) ;
}

/**
* Launch the loading of the next Picture in the model. If the model hasn't a next picture, the model load the first picture.
* This method used an Iterator to keep the next url in the model. If the user use the
*/
public function run():Void
{
if (!_it.hasNext())
{
_it.reset() ;
}
load(_it.next()) ;
}

/**
* Notify a PictureModelEvent when the model change.
*/
public function notifyChanged( e:PictureModelEvent ):Void
{
setChanged ();
notifyObservers( e );
}

/**
* Loads the picture defined bu the url specified in argument.
* @param uri the string representation of the file to load.
*/
public function load( uri:String ):Void
{
_eLoad.setUrl( uri ) ;
notifyChanged( _eLoad ) ;
}

/**
* Reset the internal Iterator of this model.
*/
public function reset():Void
{
_it.reset() ;
}

/**
* Returns the number of picture urls in the model.
* @return the number of picture urls in the model.
*/
public function size():Number
{
return _set.size() ;
}

/**
* Show the picture.
*/
public function show():Void
{
_eVisible.setVisible(true);
notifyChanged(_eVisible) ;
}

/**
* The internal PictureModelEvent used when the model is cleared.
*/
private var _eClear:PictureModelEvent ;

/**
* The internal PictureModelEvent used when a picture is loaded.
*/
private var _eLoad:PictureModelEvent ;

/**
* The internal PictureModelEvent used when property visible changed.
*/
private var _eVisible:PictureModelEvent ;

/**
* Defined the internal Iterator of this model.
*/
private var _it:Iterator ;

/**
* The internal Set of this model.
*/
private var _set:Set ;

}

Ce modèle utilise un ADT de type Set pour stocker avec la méthode insertUrl() toutes les URL des images externes nécessaires pour remplir le diaporama d'images. À noter qu'un Set est une Collection de données qui permet de s'assurer de l'unicité d'un élément dans la Collection. Ainsi il est impossible de mettre deux images ayant la même URL dans le modèle. Je vous reparlerai des Sets et de la classe HashSet dans un prochain tutoriel traitant des ADT.

Vous remarquez surement que j'utilise trois instances de la classe PictureModelEvent pour diffuser des événements à chaque modification du modèle, je trouve intéressant d'utiliser des objets de type Event basés sur les événements de VEGAS pour diffuser les informations entre l'Observable et les IObserver, même si la fonction update() des IObserver possède un événement « arg » non typé, il est plus intéressant de récupérer un événement en faisant un peu de transtypage.

La classe PictureEvent est une simple classe qui hérite de la classe BasicEvent qui permet de diffuser des informations (URL d'une image à charger, l'image doit être affichée ou cachée). J'utilise une énumération avec les constantes CLEAR, LOAD et VISIBLE pour les différents types de cet événement comme on peut le faire en AS3.

import vegas.events.BasicEvent;

/**
* The PictureModelEvent used in the observer pattern with the PictureModel.
* @author eKameleon
*/
class test.observer.diaporama.events.PictureModelEvent extends BasicEvent
{

/**
* Creates a PictureModelEvent instance.
*/
public function PictureModelEvent( type:String )
{
super(type);
}

/**
* The name of the event type when the picture model is clear.
*/
static public var CLEAR:String = "onClear" ;

/**
* The name of the event type when the picture model load a new picture.
*/
static public var LOAD:String = "onLoad" ;

/**
* The name of the event type when the picture visibility is changed.
*/
static public var VISIBLE:String = "onVisible" ;

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

/**
* Returns the url string representation of the picture.
*/
public function getUrl():String
{
return _url ;
}

/**
* Sets the url string representation of the picture.
*/
public function setUrl( uri:String ):Void
{
_url = uri ;
}

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

private var _isVisible:Boolean ;

private var _url:String ;

}

Je vais en profiter pour dessiner rapidement un petit diagramme qui représente en gros les dépendances des classes dans cet exercice.

Il ne reste plus qu'à regarder la classe PictureObserver qui sert de contrôleur pour ma petite application.

import test.observer.diaporama.events.PictureModelEvent;
import test.observer.diaporama.Picture;
import test.observer.diaporama.visitors.ClearVisitor;
import test.observer.diaporama.visitors.HideVisitor;
import test.observer.diaporama.visitors.LoaderVisitor;
import test.observer.diaporama.visitors.ShowVisitor;

import vegas.core.CoreObject;
import vegas.util.observer.IObserver;
import vegas.util.observer.Observable;

/**
* This class clear the view of a Picture instance.
* @author eKameleon
*/
class test.observer.diaporama.PictureObserver extends CoreObject implements IObserver
{

/**
* Creates a new ClearVisitor instance.
*/
public function PictureObserver( picture:Picture )
{
super();
_picture = picture ;
}

/**
* This method is called whenever the observed object is changed.
* @param o the observable object.
* @param arg an argument passed to the notifyObservers method.
*/
public function update(o:Observable, arg)
{

var event:PictureModelEvent = PictureModelEvent(arg) ;

switch (event.getType())
{

case PictureModelEvent.CLEAR :
{
_picture.accept( new ClearVisitor() ) ;
break ;
}

case PictureModelEvent.LOAD :
{
_picture.URL = event.getUrl() ;
_picture.accept( new LoaderVisitor() ) ;
break ;
}

case PictureModelEvent.VISIBLE :
{
var isVisible:Boolean = event.isVisible() ;
if (isVisible)
{
_picture.accept(new ShowVisitor()) ;
}
else
{
_picture.accept(new HideVisitor()) ;
}
break ;
}

}

}

private var _picture:Picture ;

}

Cette classe implémente donc facilement l'interface IObserver en gérant simplement dans sa méthode update() les différentes actions à exécuter en fonction du type de l'événement récupéré. Vous retrouvez l'utilisation des Validators liés à la Vue (Picture) de l'application.

Pour finir, il ne reste plus qu'à regarder le code principal de l'application dans un fichier Picture.fla que vous pouvez retrouver dans l'exemple se trouvant dans le SVN du projet dans le répertoire AS2/trunk/bin/test/vegas/util/observer/

/**
* Design Patterns Observer + Visitor with VEGAS.
* eKameleon - 2006
*/
import test.observer.diaporama.model.PictureModel ;
import test.observer.diaporama.Picture ;
import test.observer.diaporama.PictureObserver ;

// View

var view:Picture = new Picture("picture1", picture_mc, URL) ;

// Controller

var controller:PictureObserver = new PictureObserver( view ) ;

// Model

var model:PictureModel = new PictureModel() ;
model.addObserver(controller) ;

// Fill the model

model.insertUrl( "library/picture1.jpg" ) ;
model.insertUrl( "library/picture2.jpg" ) ;
model.insertUrl( "library/picture3.jpg" ) ;
model.insertUrl( "library/picture4.jpg" ) ;
model.insertUrl( "library/picture5.jpg" ) ;
model.insertUrl( "library/picture6.jpg" ) ;

trace(">> " + model + " size : " + model.size()) ;

// run the first picture in the model.

model.run() ;

this.onKeyDown = function()
{
var code:Number = Key.getCode() ;
switch(code)
{
case Key.UP :
{
model.hide() ;
break ;
}
case Key.DOWN :
{
model.show() ;
break ;
}
case Key.LEFT :
{
model.clear() ;
model.insertUrl( "library/picture1.jpg" ) ;
model.insertUrl( "library/picture3.jpg" ) ;
model.insertUrl( "library/picture6.jpg" ) ;
trace(">> " + model + " size : " + model.size()) ;
break ;
}
case Key.RIGHT :
{
model.run() ;
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 model.") ;
trace("> Press Key.RIGHT to load the next picture.")

Résumé du code sur la scène principale de l'animation :

1. Je crée la Vue, le Contrôleur et le Modèle de l'application ;

2. Je remplis le modèle avec quelques 'URL' ;

3. J'utilise la méthode run() du modèle pour lancer l'affichage de la prochaine image dans le diaporama ;

4. J'utilise la méthode clear() du modèle si je souhaite vider le modèle et rafraichir la vue.

Vous remarquez surement que j'utilise une interface IRunnable pour gérer mes commandes dans VEGAS. La classe PictureModel est donc également une commande de type IRunnable qui lui permet d'automatiser la diffusion des images par l'utilisateur.

Je ne rentre pas dans le détail du pattern Command dans ce tutoriel, mais ceux qui le connaissent le reconnaitront surement via mon interface vegas.core.IRunnable qui impose l'utilisation d'une fonction run() dans une classe (pas de paramètre dans cette méthode et cette méthode en renvoie aucune valeur)

Je vous conseille de prendre votre temps pour décortiquer ce tutoriel. Le pattern Observer reste très intéressant pour ceux qui découvrent les concepts de la POO.

Malgré tout, cette conceptualisation me dérange ! La diffusion de tous les « événements » dans la seule méthode update() de l'Objet IObserver peut devenir très lourde à la longue. Si le contrôleur doit exécuter plusieurs commandes, cette méthode risque rapidement de devenir énorme ! Dans le prochain tutoriel heureusement nous allons pouvoir facilement arranger tout cela en utilisant un modèle proche de celui utilisé par certains frameworks comme Pixlib qui permet de libérer toutes les commandes de l'application en plusieurs modules indépendants et réutilisables. À suivre donc dans le prochain épisode…