Notions avancées de la POO

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Nous allons maintenant découvrir des notions disons plus… « avancées » de la POO. Avant de démarrer ce chapitre, laissez-moi vous préciser que les concepts abordés ici sont généralement réservés aux projets complexes ou d'envergure. Il est donc fort probable que vous n'utilisiez pas ces concepts dans vos premiers projets et même que vous n'en voyez pas encore l'utilité. C'est pourquoi il n'est pas essentiel pour la suite du cours, d'être totalement au clair avec ces notions. Lisez donc attentivement ce chapitre, mais n'hésitez pas à y revenir plus tard pour effectuer une lecture plus approfondie.

Les classes dynamiques

Définition de la classe de base

Nous allons ici revenir sur la notion de classes dynamiques présentée dans le chapitre précédent. Contrairement aux classes normales dites « scellées », le principe des classes dynamiques est de pouvoir rajouter de nouvelles propriétés lors de l'exécution du programme. Bien évidemment avant d'ajouter de nouvelles propriétés, il est d'abord nécessaire de définir une classe de base, mais cette fois à l'aide du type dynamic.

Nous allons donc partir d'une classe Personnage très basique :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package
{

    dynamic class Personnage
    {

        protected var _sante:int;

        public function Personnage()
        {
            sante = 100;
        }

        public function get sante():int
        {
            return _sante;
        }

        public function set sante(value:int):void
        {
            _sante = value;
        }
    }
}

Nous en reparlerons un peu plus loin, mais notez toutefois qu'une classe dynamique respecte le principe d'encapsulation. Nous avons ainsi nos attributs qui sont restreints à la classe ainsi qu'à d'éventuelles sous-classes.

Nous pouvons ainsi instancier autant de fois qu'on veut cette classe Personnage, comme ci-dessous :

1
2
var guerrier:Personnage = new Personnage();
var magicien:Personnage = new Personnage();

Nous allons maintenant voir comment étendre les fonctionnalités de notre classe durant l'exécution en ajoutant de nouvelles propriétés à celle-ci.

Définition de propriétés hors de la classe

En pratique

Maintenant que notre classe Personnage est définie de type dynamic, nous allons pouvoir créer une instance de celle-ci, puis lui ajouter de nouvelles propriétés. Comme vous avez pu le deviner d'après la phrase précédente, la définition de nouvelles propriétés se fait à partir d'une occurrence de la classe en question. Nous allons donc prendre l'exemple d'un magicien :

1
var magicien:Personnage = new Personnage();

Ce type de personnage pourrait donc être doté de facultés magiques. Nous allons donc lui rajouter un nouvel attribut _mana qui définit la réserve de magie du personnage :

1
magicien._mana = 50;

Nous avons ainsi créé ici un nouvel attribut nommé _mana. Cette nouvelle propriété sera alors effective à l'exécution du code, et sera liée uniquement à l'occurrence magicien de la classe Personnage. C'est pourquoi un nouvel objet guerrier de cette classe, ne disposerait pas de cet attribut.

L'utilisation de classes dynamiques ne se limite pas simplement à la définition de nouveaux attributs, mais s'étend également la définition de nouvelles méthodes. Définissons par exemple une nouvelle méthode lancerSort() spécifique à l'instance magicien de la classe Personnage :

1
2
3
4
5
magicien.lancerSort = function (cible:Personnage):void
{
    cible.sante -= 25;
    _mana -= 20;
};

Nous pouvons ainsi appeler cette méthode comme n'importe quelle autre de la classe de base :

1
magicien.lancerSort(guerrier);

Comme vous pouvez le constater, les expressions de fonction sont particulièrement utiles pour la définition de méthodes de classes dynamiques.

Remarques importantes

Vous aurez certainement remarqué l’absence de droits d'accès liés aux nouvelles propriétés. En effet il n'est pas nécessaire de les spécifier, étant donné que ces propriétés sont liées à l'instance de la classe et non à la classe elle-même. Ainsi celles-ci ne sont accessibles qu'à partir de l'occurrence où elles ont été définies.

Pour revenir sur la notion d'encapsulation, les portées private et protected limitent l'accès des propriétés auxquelles elles sont associées à la classe uniquement, ou éventuellement aux classes filles. Ainsi dans le cas des classes dynamiques, la définition de nouvelles propriétés se fait à l'extérieur de la classe de base. C'est pourquoi nous n'avons pas accès aux propriétés « privées » dans la définition de ces nouvelles propriétés lors de l'exécution du code.

Pourquoi passer par des classes dynamiques alors que l'héritage permet déjà de faire tout cela ?

Il est vrai que dans la quasi-totalité des cas, vous n'aurez pas besoin de passer par la définition de classes dynamiques. D'ailleurs ces dernières ne permettent pas de séparer la définition d'une classe de son utilisation par la création d'instances. C'est pourquoi il est toujours préférable de passer par la conception de classes non dynamiques. Cependant ces classes dynamiques peuvent trouver leur utilité lorsque des propriétés dépendent de certaines données dont on ne dispose pas à la compilation. Quoi qu'il en soit, si vous ne savez pas quelle solution utiliser, préférez l'héritage aux classes dynamiques à chaque fois que cela est possible !

Les interfaces

Problème

Introduction

Pour introduire les interfaces, nous allons nous servir d'un exemple. Nous allons reprendre l'idée de créer des personnages pour un jeu quelconque. Pour cela, nous allons garder notre classe de base nommée Personnage. Cependant nous voulons dans notre jeu, non pas avoir de simples instances de la classe Personnage, mais plutôt des lutins, des elfes et des ogres qui héritent de cette classe de base. Ceux-ci possèdent alors des aptitudes différentes, mais qu'on pourrait classer dans deux catégories : Magicien et Guerrier ! :lol:

Voici donc une présentation des différentes races :

  • Lutin : ces petits bonhommes aux pouvoirs sensationnels se servent exclusivement de la magie et possèdent donc uniquement les aptitudes de la catégorie Magicien.
  • Elfes : ces êtres baignés dans la magie, ont également un certain talent dans la conception et l'utilisation d'armes ; ils sont donc à la fois Magicien et Guerrier.
  • Ogres : ces brutes tout droit sorties des grottes n'ont aucun sens de la magie, et possèdent donc seulement les aptitudes de la catégorie Guerrier.

Nous allons donc voir ici les problèmes auxquels nous allons nous heurter pour concevoir le code de ces différentes classes, toutefois nous verrons qu'il est possible de les contourner.

Les limites de l'héritage

L'objectif ici est donc de concevoir trois classes pour notre jeu : Lutin, Elfe et Ogre. Pour réaliser cela, vous pourriez avoir l'idée d'utiliser de concept d'héritage. Nous pourrions donc organiser nos différentes classes suivant un double héritage. Ainsi nous aurions donc une classe de base Personnage, dont hériteraient les deux classes suivantes : Magicien et Guerrier. Puis nos trois classes Lutin, Elfe et Ogre seraient des classes filles à ces deux dernières. Ceci ne poserait aucun problème en ce qui concerne les classes Lutin et Ogre. Seulement voilà, la classe Elfe devrait alors hériter en même temps des deux classes Magicien et Guerrier, or ceci est interdit.

En effet si dans certains langages tels que le C++ les héritages multiples sont autorisés, cette pratique est interdite en Actionscript. Ainsi une classe ne peut hériter que d'une unique superclasse !

Cependant il existe un concept en Actionscript qui permet de contourner ce problème : les interfaces !

Utilisation des interfaces

Le principe

Une interface n'est pas vraiment une classe, il s'agit plutôt d'une collection de déclarations de méthodes. Contrairement aux classes, vous ne trouverez ni attribut ni constructeur au sein d'une interface. D'autre part, le terme « déclaration » de la définition précédente nous indique que nous nous contenterons de « décrire » les méthodes. Ainsi nous définirons la manière dont doit être construite chacune des méthodes, sans en préciser le fonctionnement interne. Voici comment procéder à une déclaration d'une méthode :

1
function uneMethode(param:String):void;

Identiquement à une déclaration de variable, vous remarquerez qu'on « omet » le contenu de la méthode normalement placé entre accolades. Toutefois, nous retrouvons dans cette déclaration l'ensemble des informations utiles de la méthode, à savoir : le nom de la fonction, les différents paramètres, le type de valeur renvoyé.

Une interface sera donc un ensemble de déclarations de méthodes, regroupées autour d'une utilité commune. Nous pourrons ainsi créer de nouvelles classes qui utilisent ces interfaces et nous redéfinirons l'ensemble des méthodes. On appelle cela l'implémentation !

Quel est l'intérêt des interfaces, s'il faut redéfinir l'ensemble des méthodes ?

Étant donné que le contenu des méthodes n'est pas défini à l'intérieur des interfaces, il va bien évidemment falloir le faire dans les classes qui les implémentent. C'est pourquoi il est fort probable que vous vous demandiez à quoi pourrait bien vous servir ce concept. Je vais donc essayer de vous l'expliquer en quelques mots.

Lorsque nous avions vu la notion d'encapsulation, je vous avais précisé le fait que nous devions masquer le fonctionnement interne d'une classe à l'utilisateur de celle-ci. Ainsi l'utilisation de la classe devenait plus aisée, car nous n'avions plus qu'à manipuler des méthodes explicites qui se chargeaient du travail laborieux. Une interface va donc nous servir à décrire « l'interface » d'une pseudo-classe, c'est-à-dire présenter celle-ci telle qu'elle est vue de l'extérieur. Nous obtiendrons donc une sorte de « mode d'emploi » qui explique comment cette pseudo-classe doit être manipulée.

Nous conviendrons que le concept d'interfaces n'a d'utilité que dans de gros projets. Cela permet de définir les normes à respecter, et ainsi garantir la compatibilité des classes qui les implémenteront. Ces dernières pourront alors être utilisées de la manière, malgré les différences de contenu à l'intérieur de chacune des méthodes. Cela vient donc compléter la notion de polymorphisme et l'idée de pouvoir utiliser des objets différents comme s'il s'agissait du même.

Ce gros morceau de théorie va maintenant laisser place à la pratique qui, je suis d'accord avec vous, permet en général de mieux cerner la chose.

Les bases de la conception

À présent, nous allons reprendre notre problème de personnages concernant des lutins, des elfes et des ogres ! Parce qu'un schéma est toujours plus explicite que des mots, je vous laisse découvrir à la figure suivante l'ensemble de ces classes et interfaces que nous allons réaliser :

Les classes et les interfaces que nous allons réaliser

En premier lieu, nous allons créer une classe Personnage en tant que superclasse pour la suite. Celle-ci n'a rien d’extraordinaire puisqu'elle est identique à ce que nous avons fait précédemment. Je vous laisse néanmoins le temps de la reprendre avant de passer à la suite :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package
{

    public class Personnage
    {

        protected var _sante:int;

        public function Personnage()
        {
            sante = 100;
        }

        public function get sante():int
        {
            return _sante;
        }

        public function set sante(value:int):void
        {
            _sante = value;
        }
    }
}

Les interfaces

Nous voilà enfin au cœur du problème, nous allons devoir créer les deux interfaces Magicien et Guerrier ! Étant donné qu'il s'agit d'un exemple, nous n'allons pas réaliser des dizaines de déclarations de méthodes. Je vous propose ainsi de lier deux méthodes lancerSort et guerison à notre interface Magicien. Voici donc notre première interface :

1
2
3
4
5
6
7
8
package
{
    public interface Magicien
    {
        function lancerSort(cible:Personnage):void; 
        function guerison():void; 
    } 
}

Déclarons également deux méthodes pour notre seconde interface, nommées assenerCoup et seSoigner. En voici le résultat :

1
2
3
4
5
6
7
8
package
{
    public interface Guerrier 
    {
        function assenerCoup(cible:Personnage):void;
        function seSoigner():void;
    } 
}

À présent, nous disposons des bases nécessaires à la création de nos classes « réelles », c'est-à-dire celles que nous utiliserons dans la pratique. Nous pourrons ainsi combiner les notions d'héritage et d'implémentation pour réaliser celles-ci.

Notez que dans cet exemple nous utilisons simultanément les notions d'héritage et d'implémentation. Toutefois en pratique, il n'est pas d'obligatoire de procéder à un héritage pour utiliser les interfaces. Il est donc tout à fait possible d'utiliser uniquement la notion d'implémentation.

L'implémentation

Tout comme nous avions le mot-clé extends pour l'héritage, nous utiliserons implements pour implémenter une interface dans une classe. Étant donné que le contenu de chacune des méthodes n'est pas défini dans les interfaces, nous allons devoir le faire ici.

Je vous propose donc de découvrir les trois classes « finales » qui implémenteront donc les interfaces définies juste avant. Commençons par la classe Lutin :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package
{

    public class Lutin extends Personnage implements Magicien
    {

        public function Lutin()
        {
            super();
        }

        public function lancerSort(cible:Personnage):void
        {
            cible.sante -= 10;
            trace("Sort : Boule de feu");
        }
        public function guerison():void
        {
            this.sante += 10;
            trace("Sort : Guérison");
        } 
    }
}

La classe Elfe est sans doute la plus complexe, puisqu'elle implémente les deux interfaces à la fois :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package
{

    public class Elfe extends Personnage implements Magicien, Guerrier
    {

        public function Elfe()
        {
            super();
        }

        public function lancerSort(cible:Personnage):void
        {
            cible.sante -= 5;
            trace("Sort : Tornade");
        }

        public function guerison():void
        {
            this.sante += 5;
            trace("Sort : Guérison");
        }

        public function assenerCoup(cible:Personnage):void
        {
            cible.sante -= 5;
            trace("Coup : Épée");
        }
        public function seSoigner():void
        {
            this.sante += 5;
            trace("Soin : Herbe médicinale");
        } 
    }
}

Lorsqu'on réalise un héritage et une ou plusieurs implémentations en même temps, l'héritage doit toujours être effectué en premier. C'est pourquoi le mot-clé extends sera toujours placé avant implements dans la déclaration de la classe. Vous remarquerez également que les différentes interfaces implémentées ici sont séparées par une virgule.

Enfin pour finir, la classe Ogre ne devrait maintenant plus poser de problèmes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package
{

    public class Ogre extends Personnage implements Guerrier
    {

        public function Ogre()
        {
            super();
        }

        public function assenerCoup(cible:Personnage):void
        {
            cible.sante -= 10;
            trace("Coup : Hache de guerre");
        }
        public function seSoigner():void
        {
            this.sante += 10;
            trace("Soin : Bandage");
        } 
    }
}

Sachant que les méthodes ne sont que déclarées à l'intérieur des interfaces, celles-ci doivent obligatoirement être redéfinies dans les classes qui les implémentent. Veillez donc à bien toutes les redéfinir sous peine de messages d'erreurs lors de la compilation de votre projet.

Plus loin avec les interfaces

Nous avons vu le principe des interfaces, mais je vais ici vous présenter une utilisation qui en fait tout leur intérêt !

Imaginez maintenant que vous vouliez créer un jeu beaucoup plus complet, qui risque d'évoluer dans une version ultérieure. Vous voulez cette fois multiplier le nombre des races des personnages avec, par exemple, les classes Humain, Nain, Lutin, Elfe, Centaure, Minotaure, Ogre, Gnome et Troll. Cette fois vous n'avez plus de simples magiciens ou de purs guerriers, mais chacune des races dispose de l'ensemble des facultés. Ainsi chaque espèce est capable de lancer des sorts ou de se battre avec n'importe quel type d'armes. En revanche, chacun possède une liste limitée de sorts au départ, mais peut la développer au fil du jeu. Tous ces sorts sont utilisés de la manière, seuls leurs effets et leurs dégâts sont différents de l'un à l'autre.

Une architecture judicieuse de vos classes serait alors de créer une interface Sort, qui serait implémentée par chacun de vos divers sorts. L'intérêt ici serait alors de pouvoir créer une liste de sort de type Vector.<Sort> comme attribut de votre classe Personnage. Chaque sort serait alors utilisé de la même façon quel que soit celui-ci.

Voici un schéma à la figure suivante qui résume la situation et qui devrait plus facilement vous séduire.

Créez une interface « Sort »

Vous pouvez ainsi créer un nouvel Ogre nommé budoc. Héritant de la classe Personnage, votre personnage va pouvoir apprendre de nouveaux sorts au fil du jeu. Voici par exemple notre ami budoc qui apprend les sorts Guerison et Eclair :

1
2
3
var budoc:Ogre = new Ogre();
budoc.ajouterSort(new Guerison());
budoc.ajouterSort(new Eclair());

Je vous ai présenté le concept avec les interfaces, mais sachez que cette pratique peut également être utilisée avec la relation d'héritage, même si cela est moins courant. Au risque de me répéter encore une fois, une des grandes forces de la POO est justement de pouvoir se servir d'objets différents de la même manière grâce à ces techniques d'héritage, de polymorphisme et d'implémentation.

Nous sommes maintenant arrivés au bout de ces explications sur les interfaces. J'espère vous avoir convaincus de leur utilité et j'ose croire que vous les utiliseraient de la bonne façon.

Les classes abstraites

Le concept

Une classe abstraite est une classe que l'on ne peut pas instancier, c'est-à-dire que l'on ne peut pas directement créer d'objet de cette classe avec le mot-clé new. Lorsque l'on choisit de rendre une classe abstraite, on restreint ainsi ses possibilités d'utilisation. Par contre, les sous-classes de cette classe abstraite ne sont pas forcément abstraites, à moins de l'indiquer à chaque fois !

Pour comprendre pourquoi cela pourrait améliorer la cohérence de votre code, reprenons l'exemple des véhicules sur l'autoroute (voir figure suivante).

Diagramme UML des classes Vehicule et Voiture

Il était alors possible de créer directement un véhicule :

1
var vehicule:Vehicule = new Vehicule('Renault', 0);

Maintenant, si notre application ne s'occupe que de voitures, il serait inutile d'utiliser directement la classe Vehicule alors que nous disposons de sa sous-classe Voiture. Si nous laissions à d'autres programmeurs la possibilité de créer des véhicules au lieu de voitures, ils auraient une chance sur deux de se tromper et d'oublier d'utiliser la classe Voiture. Pour cela, il faut rendre la classe Vehicule abstraite (voir figure suivante) !

Par opposition à une classe abstraite, une classe qui peut être instanciée est une classe concrète.

Diagramme UML de la classe abstraite Vehicule et de sa sous-classe

Rappel : une sous-classe d'une classe abstraite n'est pas forcément abstraite ; il faut le spécifier à chaque fois. Dans notre exemple, on peut donc instancier la classe Voiture, mais pas la classe Vehicule.

Ainsi, nous sommes désormais obligés d'utiliser la classe Voiture pour créer un objet voiture : notre code est cohérent ! :)

1
2
3
var vehicule:Vehicule = new Vehicule('Renault', 0); // Interdit !

var voiture:Voiture = new Voiture('Renault', 0, 'BX6F57'); // Autorisé :)

Application à l'ActionScript 3

Malheureusement, ce principe de classes abstraites ne dispose pas de mécanismes officiels au sein du langage ; il faut donc le « programmer » soi-même. Cela ne signifie pas qu'il ne faut pas l'utiliser, et plusieurs classes fournies par Flash sont des classes abstraites ! Mais rassurez-vous, c'est en réalité très simple. ;)

Le principe est le suivant : nous allons faire une vérification dans le constructeur de la classe-mère abstraite, à l'aide d'un paramètre et du mot-clé this. En effet, seule une sous-classe de cette classe peut passer en paramètre du constructeur le mot-clé this !

Voici le code de la classe abstraite :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package  
{
    import flash.errors.IllegalOperationError;
    /**
     * Une classe abstraite
     */
    public class MaClasseAbstraite 
    {

        public function MaClasseAbstraite(moi:MaClasseAbstraite) 
        {
            // Nous vérifions si le paramètre de vérification correspond à l'objet
            if (moi != this)
            {
                // Sinon, quelqu'un tente d'instancier cette classe, nous envoyons donc une erreur
                // car seule une sous-classe peut passer le mot-clé 'this' en paramètre au constructeur
                throw new IllegalOperationError("MaClasseAbstraite est une classe abstraite et ne peut donc pas être directement instanciée.");
            }
        }

    }

}

Le mot-clé throw permet d'envoyer une erreur ; ici IllegalOperationError, qui indique qu'il s'agit d'une opération illégale. Pour l'instant il n'est pas nécessaire que vous vous attardiez sur cette instruction, puisque nous reviendrons sur la gestion des erreurs dans un chapitre spécialisé, plus loin de ce cours.

Nous demandons dans le constructeur un paramètre moi obligatoire, du même type que la classe elle-même : ainsi, seuls les objets des sous-classes de MaClasseAbstraite seront acceptés. Ensuite, nous vérifions que ce paramètre pointe vers le même objet, c'est-à-dire que cet objet qui vient d'être créé est bien une instance d'une sous-classe. :)

Rappel : le mot-clé this est une référence qui pointe sur l'unique objet qui fait actuellement travailler le corps de la classe. Même en remontant entre les sous-classes, cette référence pointe toujours sur le même objet (qui appartient à toutes ces classes en même temps, comme nous l'avons vu dans le chapitre sur l'héritage).

Voici le code de la sous-classe concrète :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package  
{
    /**
     * Une sous-classe concrète.
     */
    public class MaClasseConcrete extends MaClasseAbstraite 
    {

        public function MaClasseConcrete() 
        {
            // On envoie la référence de l'objet qui vient d'être créé à la classe mère pour passer la vérification
            super(this);

        }

    }

}

Dans le constructeur de la classe-fille concrète, nous appelons le constructeur de la classe-mère abstraite à l'aide du mot-clé super pour passer la vérification.

Voici ce que cela donne lorsque nous voulons créer un objet de chaque classe :

1
2
3
var objet1:MaClasseAbstraite = new MaClasseAbstraite(); // Une erreur va être envoyée

var objet2:MaClasseConcrete = new MaClasseConcrete(); // Tout va bien.

Les types inconnus

Déterminer si un objet est une occurrence d'une certaine classe

Le mot-clé is permet de renvoyer un booléen pour savoir si un objet quelconque est une occurrence d'une certaine classe, ou d'une sous-classe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var entier:int = -23;
var entierPositif:uint = 42;
var nombre:Number = 3.14;
var chaine:String = 'Hello world!';

// entier est du type int
trace(entier is int); // Affiche: true
// entier n'est pas du type uint
trace(entier is uint); // Affiche: false
// La classe int est une sous-classe de Number
trace(entier is Number); // Affiche: true

trace(entierPositif is uint); // Affiche: true
// La classe uint est aussi une sous-classe de Number
trace(entierPositif is Number); // Affiche: true

trace(nombre is int); // Affiche: false
trace(nombre is Number); // Affiche: true

trace(chaine is String); // Affiche: true
trace(chaine is int); // Affiche: false

Cela fonctionne également avec les fonctions ! Car il faut toujours se rappeler qu'en ActionScript 3, tout est objet : les variables, les fonctions, les objets de la classe Voiture, leurs attributs, leurs méthodes…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function maFonction():void
{
    trace('Je suis dans une fonction !');
}

// Toute fonction est une instance de la classe Function !
trace(maFonction is Function); // Affiche: true
// Mais chaine n'est pas une fonction :p
trace(chaine is Function); // Affiche: false

// Tout objet est une instance de la classe Object !
trace(entier is Object); // Affiche: true
trace(chaine is Object); // Affiche: true
trace(maFonction is Object); // Affiche: true

Des paramètres de type inconnu

Dans les fonctions, il est possible de demander des paramètres de type inconnu, c'est-à-dire dont nous ne pouvons pas connaître la classe à l'avance ! Pour cela, nous pouvons utiliser la classe Object, qui est la classe mère de tous les objets en ActionScript 3.

1
2
3
4
5
6
7
function quiSuisJe(quelqueChose:Object):void
{
    if(quelqueChose is Function)
    {
        trace('Je suis une fonction !');
    }
}

Il existe un raccourci pour spécifier que le type d'un paramètre est inconnu : le caractère « * » ! Reprenons l'exemple précédent en utilisant ce raccourci :

1
2
3
4
5
6
7
function quiSuisJe(quelqueChose:*):void
{
    if(quelqueChose is Function)
    {
        trace('Je suis une fonction !');
    }
}

Essayons cette fonction avec une autre fonction :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function maFonction():void
{
    trace('Je suis dans une fonction !');
}

function quiSuisJe(quelqueChose:*):void
{
    if(quelqueChose is Function)
    {
        trace('Je suis une fonction !');
    }
}

// Nous passons l'objet maFonction de classe Function en paramètre !
quiSuisJe(maFonction); // Affiche: Je suis une fonction !

L'objet maFonction est une occurrence de la classe Function (étant donné que c'est une fonction), donc le test est positif et le message « Je suis une fonction ! » est affiché.

Nous pourrions ensuite appeler la fonction à l'aide de la méthode call() de la classe Function :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function maFonction():void
{
    trace('Je suis dans une fonction !');
}

function quiSuisJe(quelqueChose:*):void
{
    if(quelqueChose is Function)
    {
        trace('Je suis une fonction !');
        // On appelle la fonction !
        quelqueChose.call();
    }
}

// Nous passons l'objet maFonction de classe Function en paramètre
quiSuisJe(maFonction); // Affiche: Je suis une fonction ! Je suis dans une fonction !

Accéder dynamiquement aux propriétés

En ActionScript 3, il est possible d'accéder à une propriété d'un objet en connaissant son nom uniquement à l'exécution. Par exemple, vous obtenez le nom d'un attribut dans une variable de type String, et vous voulez ensuite modifier sa valeur pour faire une animation ou autre chose.

Prenons une classe Voiture disposant d'un attribut vitesse et d'une méthode accelerer :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package
{
    public class Voiture
    {

        private var _vitesse:int;

        public function Voiture()
        {
            vitesse = 10;
        }

        public function get vitesse():int 
        {
            return _vitesse;
        }

        public function set vitesse(value:int):void 
        {
            _vitesse = value;
        }

        public function accelerer(combien:int = 1):void
        {
            vitesse += combien;
        }
    }

}

La syntaxe à respecter pour accéder à l'attribut de nom 'vitesse' est la suivante :

1
2
3
4
var objet:Voiture = new Voiture();
var nomDeLaPropriete:String = 'vitesse';

trace(objet[nomDeLaPropriete]); // Affiche la vitesse de la voiture: 10

Ou plus simplement :

1
2
var objet:Voiture = new Voiture();
trace(objet['vitesse']); // Affiche: 10

Rappel : une propriété constituée d'un ou plusieurs accesseurs est considérée comme un attribut.

En guise d'exemple, supposons que nous voulons disposer d'une fonction qui augmente de 10 n'importe quel attribut numérique d'un objet quelconque, sans avoir besoin de plus de précisions concernant sa classe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function augmenter(objet:Object, propriete:String):void
{
    // On vérifie le type de la propriété : il faut que ce soit un nombre
    if(objet[propriete] is int || objet[propriete] is uint || objet[propriete] is Number)
    {
        objet[propriete] += 10;
    }
    else
    {
        trace("Attention : La propriété " + propriete + " sur l'objet " + objet + " n'est pas un nombre.");
    }
}

Écrivons un petit programme principal qui crée un objet Voiture et qui utilise cette fonction pour augmenter sa vitesse :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package
{
    import flash.display.Sprite;
    import flash.events.Event;

    /**
     * Programme principal
     */
    public class Main extends Sprite
    {

        public function Main():void
        {
            if (stage)
                init();
            else
                addEventListener(Event.ADDED_TO_STAGE, init);
        }

        private function init(e:Event = null):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            // entry point

            // On créé une voiture
            var voiture:Voiture = new Voiture();
            // On initialise sa vitesse à 10
            voiture.vitesse = 10;
            // On augmente sa vitesse de 10
            augmenter(voiture, 'vitesse');
            trace(voiture.vitesse); // Affiche: 20

        }

        public function augmenter(objet:Object, propriete:String):void
        {
            // On vérifie le type de la propriété : il faut que ce soit un nombre
            if (objet[propriete] is int || objet[propriete] is uint || objet[propriete] is Number)
            {
                objet[propriete] += 10;
            }
            else
            {
                trace("Attention : La propriété " + propriete + " sur l'objet " + objet + " n'est pas un nombre.");
            }
        }

    }

}

Est-ce que cela fonctionne pour les méthodes ?

La réponse est oui, nous pouvons aussi le faire pour les méthodes ! Il suffit d'ajouter des paramètres comme pour n'importe quel appel de fonction. Par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var voiture:Voiture = new Voiture();
voiture.vitesse = 10;

// Appelons la méthode 'accelerer' de notre objet voiture !
voiture['accelerer'](5);
trace(voiture.vitesse); // Affiche: 15

// Ce qui revient à écrire :
voiture.accelerer(5);
trace(voiture.vitesse); // Affiche: 20

// Avec les accesseurs, la propriété vitesse est concidérée comme un attribut
trace(voiture['vitesse'] is Function); // Affiche: false
// Alors que accelerer est bien une méthode
trace(voiture['accelerer'] is Function); // Affiche: true

Un exemple plus concret ; nous pourrions coder une fonction qui anime une propriété numérique d'un objet d'une valeur à une autre, pendant un certain temps, mais malheureusement, nous ne sommes pas encore assez loin dans le cours pour pouvoir faire cela. Toutefois, je vous montre à quoi pourrait ressembler la signature de la fonction et un exemple d'utilisation :

1
2
3
4
function animer(objet:Object, propriete:String, valeurDepart:*, valeurArrivee:*, duree:Number):void
{
    // Animation à l'aide de objet[propriete]
}

J'utilise la notation avec l'étoile pour le type de valeurDepart et valeurArrivee pour que l'on puisse passer des nombres de type int, uint ou Number. Attention toutefois à penser à vérifier leurs types dans le corps de la fonction.

Dans notre classe Main, nous pourrions écrire :

1
2
3
4
var voiture:Voiture = new Voiture();

// Accélérons !
animer(voiture, 'vitesse', 0, 100, 3); // Nous animons la vitesse de l'objet voiture de 0 à 100 pendant 3 secondes

Avec cette fonction, nous pourrions donc animer n'importe quel attribut de n'importe quel objet ! :magicien:

Les objets anonymes

Créer un objet anonyme

Un objet anonyme est un objet dynamique qui n'a pas de classe (autre que la classe Object). On peut modifier ses propriétés de manière totalement libre : créer de nouveaux attributs ou de nouvelles méthodes, les modifier après coup… En effet, ses propriétés ne sont pas décrite dans une classe particulière, donc on peut faire ce que l'on veut (ce qui peut parfois poser problème, où l'on devra effectuer de nombreux tests sur l'objet anonyme car on ne sait pas à l'avance comment il sera structuré).

Pour créer un objet anonyme, il y a deux façons de procéder. La première est de créer une instance de la classe Object comme ceci :

1
var objetAnonyme:Object = new Object();

Puis on peut lui affecter des propriétés librement :

1
2
3
4
// Ajoutons quelques attributs
objetAnonyme.nom = "Zeste de Savoir";
objetAnonyme.adresse = "zestedesavoir.com";
trace(objet.nom + " : " + objet.adresse);

Nous obtenons dans la console :

1
Zeste de Savoir : zestedesavoir.com

On peut également créer un objet anonyme avec des accolades en spécifiant directement ses propriétés séparées par des virgules ainsi :

1
2
3
4
5
6
{
    nomPropriete1:valeur1,
    nomPropriete2:valeur2,
    nomPropriete3:valeur3,
    ...
}

Reprenons notre exemple en utilisant cette méthode :

1
2
3
4
5
var objet:Object = {
    nom: "Zeste de Savoir",
    adresse: "zestedesavoir.com"
};
trace(objet.nom + " : " + objet.adresse);

Ce qui nous donne à nouveau :

1
Zeste de Savoir : zestedesavoir.com

Il peut parfois être utile, comme dans le cas où une fonction attend beaucoup de paramètres facultatifs. Par exemple, nous pourrions avoir une fonction attendant neuf paramètres facultatifs (j'ai volontairement mis en forme les paramètres pour qu'ils soient plus lisibles) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function creerEmailBrouillon(
    objet:String = "", 
    message:String = "", 
    date:Date = null, 
    emailsDestinataires:Array = null, 
    emailsCC:Array = null, 
    emailsCCI:Array = null,
    emailExpediteur:String = "",
    piecesJointes:Array = null,
    demanderConfirmationLecture:Boolean = false
):void
{
    // Code ici
}

Cette fonction créé un brouillon d'e-mail : aucun champ n'est obligatoire car tous les paramètres ont une valeur par défaut. En effet, lorsque vous enregistrez un brouillon d'e-mail, vous n'êtes pas obligé de remplir tous les champs. Nous pouvons donc appeler cette fonction sans lui passer de paramètre :

1
creerEmailBrouillon(); // Tous les paramètres auront leur valeur par défaut

Mais que devrions-nous faire si nous ne voulons enregistrer que l'e-mail de l'expéditeur seul ? Et bien, il faudrait spécifier quand-même des valeurs pour les paramètres précédent le paramètre emailExpediteur, ce qui serait fastidieux et peu commode ! Voyez plutôt :

1
creerEmailBrouillon("", "", null, null, null, null, "clem@zestedesavoir.com");

Pour remédier à ce problème, nous pouvons n'utiliser qu'un seul paramètre : un objet anonyme !

1
2
3
4
function creerEmailBrouillon(options:Object = null):void
{
    // Code ici
}

Nous pouvons maintenant nous contenter de passer uniquement les paramètres qui nous intéressent lors de l'appel de la fonction :

1
2
3
creerEmailBrouillon({
    emailExpediteur:"clem@zestedesavoir.com"
});

Il faut avouer que c'est tout de même plus lisible et moins fastidieux à taper. Dans la fonction creerEmailBrouillon, il va falloir faire plusieurs tests sur les paramètres, afin de savoir s'ils ont été passés avant de les utiliser. On commence d'abord par tester l'existence de l'objet options, puis on regarde si chaque paramètre est défini, c'est-à-dire s'il est différent de undefined (indéfini en anglais) et si son type est correct.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function creerEmailBrouillon(options:Object = null):void
{
    if (options) // Equivaut à : options != null
    {
        // On vérifie si le paramètre objet est défini et est de type String
        if (options.objet != undefined && options.objet is String)
        {
            // Traiter l'objet
            trace("Objet: " + options.objet);
        }

        if (options.message != undefined && options.message is String)
        {
            // Traiter le message
            trace("Message: " + options.message);
        }

        if (options.date != undefined && options.date is Date)
        {
            // Traiter la date de l'email
            trace("Date: " + options.date);
        }

        if (options.emailsDestinataires != undefined && options.emailsDestinataires is Array)
        {
            // Traiter la liste des destinataires
            trace("Emails destinataires: " + options.emailsDestinataires.join(", ")); // On affiche les emails séparés par des virgules grâce au join(",")
        }

        if (options.emailsCC != undefined && options.emailsCC is Array)
        {
            // Traiter la liste des destinataires en copie
            trace("Emails destinataires en copie: " + options.emailsCC.join(", "));
        }

        if (options.emailsCCI != undefined && options.emailsCCI is Array)
        {
            // Traiter la liste des destinataires en copie cachée
            trace("Emails destinataires en copie cachée: " + options.emailsCCI.join(", "));
        }

        if (options.emailExpediteur != undefined && options.emailExpediteur is String)
        {
            // Traiter d'adresse de l'expéditeur
            trace("Email de l'expéditeur: " + options.emailExpediteur);
        }

        if (options.piecesJointes != undefined && options.piecesJointes is Array)
        {
            // Traiter la liste des pièces jointes
            trace("URL des pièces jointes: " + options.piecesJointes.join(", "));
        }

        if (options.demanderConfirmationLecture != undefined && options.demanderConfirmationLecture is Boolean)
        {
            // Traiter l'option de demande de confirmation de lecture
            trace("Confirmation de lecture: " + options.demanderConfirmationLecture);
        }
    }

    // Suite éventuelle du code ici
}

Il est nécessaire de bien documenter cette fonction en détaillant les différents paramètres afin que l'on puisse par la suite l'utiliser facilement.

Avec ce squelette de code, nous obtenons dans la console :

1
Email de l'expéditeur: clem@zestedesavoir.com

Seul le paramètre emailExpediteur a été détecté et son type a bien été validé.

Essayons de passer plusieurs paramètres :

1
2
3
4
5
creerEmailBrouillon({
    date:new Date(), // Date d'aujourd'hui
    emailsDestinataires:["titi@zestedesavoir.com", "tata@zestedesavoir.com", "toto@zestedesavoir.com"], // Destinataires
    emailExpediteur:"clem@zestedesavoir.com" // Expéditeur
});

Ce qui nous donne dans la console :

1
2
3
Date: Wed Nov 6 16:11:31 GMT+0100 2013
Emails destinataires: titi@zestedesavoir.com, tata@zestedesavoir.com, toto@zestedesavoir.com
Email de l'expéditeur: clem@zestedesavoir.com

Les fonctions anonymes

Une fonction anonyme est une fonction dont on ne précise pas le nom. Par exemple, cette fonction est anonyme :

function():void { trace("Hello world!"); }

Comme vous pouvez le constater, cette fonction n'a pas de nom définit entre le mot-clé function et les parenthèses des paramètres. On ne peut donc pas l'appeler directement. Par contre, on peut l'affecter à une variable de type Function comme ceci :

1
2
3
4
var hello:Function = function():void
{
    trace("Hello world!");
}

Pour l'appeler, on considère que la variable est directement la fonction :

1
hello(); // Appelle la fonction contenue dans la variable hello

Nous pouvons ainsi créer des méthodes sur un objet anonyme en affectant une fonction anonyme à une propriété de cet objet :

1
2
3
4
5
6
var anonyme:Object = new Object();
anonyme.hello = function():void
{
    trace("Hello world!");
};
anonyme.hello(); // Affiche : Hello world!

  • Il est possible de créer des classes dynamiques que l'on peut modifier durant l'exécution, grâce au type dynamic.
  • Les interfaces permettent d'hériter en quelque sorte de plusieurs classes, pour partager des propriétés communes.
  • Nos classes peuvent être abstraites : dans ce cas, on renvoie une erreur si quelqu'un essaye d'en créer une instance.
  • Le type des variables que nous manipulons peut être indéfini ; on peut alors le tester à l'aide du mot-clé is.
  • Il est possible d'accéder dynamiquement à des propriétés dont on connaît le nom.