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 :
- Je crée la Vue, le Contrôleur et le Modèle de l'application ;
- Je remplis le modèle avec quelques 'URL' ;
- J'utilise la méthode run() du modèle pour lancer l'affichage de la prochaine image dans le diaporama ;
- 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…