Exemple : GeometricShapes

L’exemple d’application GeometricShapes montre comment il est possible d’appliquer un certain nombre de concepts et fonctionnalités orientés objet à l’aide d’ActionScript 3.0 et, en particulier :

  • Définition des classes

  • Extension des classes

  • Polymorphisme et le mot-clé override

  • Définition, extension et implémentation des interfaces

Cet exemple comprend aussi une « méthode usine (Factory) » qui crée des occurrences de classes, montrant ainsi comment déclarer une valeur de retour comme occurrence d’une interface et utiliser l’objet ainsi renvoyé de manière générique.

Pour obtenir les fichiers de cet exemple d’application, voir la page www.adobe.com/go/learn_programmingAS3samples_flash_fr. Les fichiers de l’application GeometricShapes se trouvent dans le dossier Samples/GeometricShapes. L’application se compose des fichiers suivants :

Fichier

Description

GeometricShapes.mxml

ou

GeometricShapes.fla

Le fichier d’application principal dans Flash (FLA) ou Flex (MXML).

com/example/programmingas3/geometricshapes/IGeometricShape.as

Interface de base définissant les méthodes qui doivent être implémentées par toutes les classes de l’application GeometricShapes.

com/example/programmingas3/geometricshapes/IPolygon.as

Interface définissant les méthodes qui doivent être implémentées par les classes de l’application GeometricShapes qui comportent plusieurs côtés.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Type de forme géométrique dont les côtés sont de longueur égale et positionnés symétriquement autour du centre de la forme.

com/example/programmingas3/geometricshapes/Circle.as

Type de forme géométrique qui définit un cercle.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Sous-classe de RegularPolygon qui définit un triangle équilatéral.

com/example/programmingas3/geometricshapes/Square.as

Sous-classe de RegularPolygon qui définit un carré.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Classe contenant une méthode usine (Factory) pour créer des formes à partir d’un type et d’une taille de forme.

Définition des classes de GeometricShapes

L’application GeometricShapes permet de spécifier un type de forme géométrique et la taille de cette dernière. Elle renvoie alors la description de la forme, sa surface et son périmètre.

L’interface utilisateur de l’application est banale : elle se compose de quelques contrôles permettant de sélectionner le type de forme, de définir la taille et d’afficher la description. De fait, la partie intéressante de cette application est la face cachée de l’iceberg : la structure des classes et des interfaces elles-mêmes.

Cette application traite des formes géométriques, mais elle ne les affiche pas graphiquement.

Les classes et interfaces qui définissent les formes géométriques de cet exemple sont représentées dans le schéma ci-dessous, en notation UML (Unified Modeling Language) :

GeometricShapes Example Classes

Définition d’un comportement commun à l’aide d’interfaces

Cette application GeometricShapes gère trois types de formes : cercles, carrés et triangles équilatéraux. La structure des classes de GeometricShapes commence par une interface très simple, IGeometricShape, qui répertorie des méthodes communes aux trois types de formes :

package com.example.programmingas3.geometricshapes 
{ 
    public interface IGeometricShape 
    { 
        function getArea():Number; 
        function describe():String; 
    } 
}

L’interface définit deux méthodes : la méthode getArea() qui calcule et renvoie la surface de la forme, et la méthode describe() qui assemble une description textuelle des propriétés de la forme.

Il est aussi souhaitable de connaître le périmètre de chaque forme. Toutefois, le périmètre d’un cercle est appelé circonférence et il est calculé de façon particulière, si bien que le comportement diverge de ceux d’un triangle ou d’un carré. Il existe cependant assez de similitudes entre les triangles, les carrés et les autres polygones pour qu’il soit logique de définir une nouvelle classe d’interface pour eux : IPolygon. L’interface IPolygon est également plutôt simple, comme on peut le constater :

package com.example.programmingas3.geometricshapes 
{ 
    public interface IPolygon extends IGeometricShape 
    { 
        function getPerimeter():Number; 
        function getSumOfAngles():Number; 
    } 
}

Cette interface définit deux méthodes communes à tous les polygones : la méthode getPerimeter() qui mesure la longueur combinée de tous les côtés et la méthode getSumOfAngles() qui additionne tous les angles intérieurs.

L’interface IPolygon étend l’interface IGeometricShape, si bien que toute classe qui implémente l’interface IPolygon doit déclarer les quatre méthodes : les deux de l’interface IGeometricShape et les deux de l’interface IPolygon.

Définition des classes de formes

Dès que vous avez une vue claire des méthodes communes à chaque type de forme, vous pouvez définir les classes de formes elles-mêmes. En ce qui concerne le nombre de méthodes à implémenter, la forme la plus simple est la classe Circle :

package com.example.programmingas3.geometricshapes 
{ 
    public class Circle implements IGeometricShape 
    { 
        public var diameter:Number; 
         
        public function Circle(diam:Number = 100):void 
        { 
            this.diameter = diam; 
        } 
         
        public function getArea():Number 
        { 
            // The formula is Pi * radius * radius. 
            var radius:Number = diameter / 2; 
            return Math.PI * radius * radius; 
        } 
         
        public function getCircumference():Number 
        { 
            // The formula is Pi * diameter. 
            return Math.PI * diameter; 
        } 
         
        public function describe():String 
        { 
            var desc:String = "This shape is a Circle.\n"; 
            desc += "Its diameter is " + diameter + " pixels.\n"; 
            desc += "Its area is " + getArea() + ".\n"; 
            desc += "Its circumference is " + getCircumference() + ".\n"; 
            return desc; 
        } 
    } 
}

La classe Circle implémente l’interface IGeometricShape. Elle doit donc comporter du code pour les méthodes getArea() et describe(). De plus, elle définit la méthode getCircumference() qui est unique à la classe Circle. La classe Circle déclare aussi une propriété, diameter, qui n’apparaîtra pas dans les autres classes, dédiées aux polygones.

Les deux autres types de formes, les carrés et les triangles équilatéraux, ont d’autres points communs : ils ont tous deux des côtés de longueur égale et il existe des formules communes pour calculer leurs périmètre et la somme de leurs angles intérieurs. En fait, ces formules communes s’appliquent à tout autre polygone régulier que vous définissez par la suite.

La classe RegularPolygon est la superclasse de la classe Square et de la classe EquilateralTriangle. Une superclasse permet de définir en un seul point des méthodes communes. Il n’est donc pas nécessaire de les définir séparément dans chaque sous-classe. Voici le code de la classe RegularPolygon :

package com.example.programmingas3.geometricshapes 
{ 
    public class RegularPolygon implements IPolygon 
    { 
        public var numSides:int; 
        public var sideLength:Number; 
         
        public function RegularPolygon(len:Number = 100, sides:int = 3):void 
        { 
            this.sideLength = len; 
            this.numSides = sides; 
        } 
         
        public function getArea():Number 
        { 
            // This method should be overridden in subclasses. 
            return 0; 
        } 
         
        public function getPerimeter():Number 
        { 
            return sideLength * numSides; 
        } 
         
        public function getSumOfAngles():Number 
        { 
            if (numSides >= 3) 
            { 
                return ((numSides - 2) * 180); 
            } 
            else 
            { 
                return 0; 
            } 
        } 
         
        public function describe():String 
        { 
            var desc:String = "Each side is " + sideLength + " pixels long.\n"; 
            desc += "Its area is " + getArea() + " pixels square.\n"; 
            desc += "Its perimeter is " + getPerimeter() + " pixels long.\n";  
            desc += "The sum of all interior angles in this shape is " + getSumOfAngles() + " degrees.\n";  
            return desc; 
        } 
    } 
}

La classe RegularPolygon déclare d’abord deux propriétés qui sont communes à tous les polygones réguliers : la longueur de chaque côté (la propriété sideLength) et le nombre de faces (la propriété numSides).

La classe RegularPolygon implémente l’interface IPolygon et déclare les quatre méthodes de l’interface IPolygon. Elle implémente deux d’entre elles, les méthodes getPerimeter() et getSumOfAngles() à l’aide de formules communes.

La formule de la méthode getArea() étant différente d’une forme à l’autre, la version de la méthode dans la classe de base ne peut pas comporter une logique commune dont hériteraient les méthodes des sous-classes. Elle renvoie donc simplement une valeur 0 par défaut pour indiquer que la surface n’a pas été calculée. Pour calculer correctement la surface de chaque forme, les sous-classes de la classe RegularPolygon doivent redéfinir elles-mêmes la méthode getArea().

Le code de la classe EquilateralTriangle, ci-dessous, montre comment la méthode getArea() :

package com.example.programmingas3.geometricshapes  
{ 
    public class EquilateralTriangle extends RegularPolygon 
    { 
        public function EquilateralTriangle(len:Number = 100):void 
        { 
            super(len, 3); 
        } 
         
        public override function getArea():Number 
        { 
        // The formula is ((sideLength squared) * (square root of 3)) / 4. 
        return ( (this.sideLength * this.sideLength) * Math.sqrt(3) ) / 4; 
        } 
         
        public override function describe():String 
        { 
                 /* starts with the name of the shape, then delegates the rest 
                 of the description work to the RegularPolygon superclass */ 
        var desc:String = "This shape is an equilateral Triangle.\n"; 
        desc += super.describe(); 
        return desc; 
        } 
    } 
}

Le mot-clé override indique que la méthode EquilateralTriangle.getArea() redéfinit volontairement la méthode getArea() de la superclasse RegularPolygon. Lorsque la méthode EquilateralTriangle.getArea() est appelée, elle calcule la surface à l’aide de la formule du code précédent. Le code de la méthode RegularPolygon.getArea() n’est jamais exécuté.

Par contre, la classe EquilateralTriangle ne définit pas sa propre version de la méthode getPerimeter(). Lorsque la méthode EquilateralTriangle.getPerimeter() est appelée, l’appel remonte la chaîne d’héritage et exécute le code de la méthode getPerimeter() de la superclasse RegularPolygon.

Le constructeur EquilateralTriangle() utilise l’instruction super() pour invoquer explicitement le constructeur RegularPolygon() de sa superclasse. Si les deux constructeurs avaient le même ensemble de paramètres, il serait possible d’omettre complètement le constructeur EquilateralTriangle et il suffirait d’exécuter le constructeur RegularPolygon(). Toutefois, le constructeur RegularPolygon() nécessite un paramètre supplémentaire, numSides. Le constructeur EquilateralTriangle() appelle donc super(len, 3) qui transmet le paramètre en entrée len et la valeur 3 pour indiquer que le triangle possède trois côtés.

La méthode describe() utilise également l’instruction super(), mais de façon différente, afin d’appeler la version de la méthode describe() figurant dans la superclasse RegularPolygon. La méthode EquilateralTriangle.describe() définit d’abord la variable de chaîne desc qui affiche le type de forme. Elle obtient ensuite les résultats de la méthode RegularPolygon.describe() en appelant super.describe() et elle ajoute ces résultats à la fin de la chaîne desc.

La classe Square n’est pas décrite en détail ici, mais elle est similaire à la classe EquilateralTriangle, avec un constructeur et ses propres implémentations des méthodes getArea() et describe().

Polymorphisme et méthode usine (Factory)

Il est possible d’utiliser de diverses façons intéressantes un ensemble de classes qui utilisent à bon escient les interfaces et l’héritage. Par exemple, toutes les classes de formes décrites jusqu’ici implémentent l’interface IGeometricShape ou étendent une superclasse qui se charge de cette implémentation. Si vous définissez une variable comme étant une occurrence de IGeometricShape, il n’est pas nécessaire, pour appeler sa méthode describe(), de savoir si c’est une occurrence de la classe Circle ou de la classe Square.

Le code suivant illustre cette situation :

var myShape:IGeometricShape = new Circle(100); 
trace(myShape.describe());

Lorsque myShape.describe() est appelée, elle exécute la méthode Circle.describe(); car bien que la variable soit définie comme une occurrence de l’interface IGeometricShape, Circle est sa classe sous-jacente.

Cet exemple illustre le principe du polymorphisme : le même appel à une méthode provoquera l’exécution d’un code différent, selon la classe de l’objet dont la méthode est appelée.

L’application GeometricShapes applique ce type de polymorphisme basé sur l’interface à l’aide d’une version simplifiée d’un modèle de conception appelé méthode usine (Factory). L’expression méthode usine (Factory) désigne une fonction qui renvoie un objet dont le type de données sous-jacent ou le contenu diffèrent selon le contexte.

La classe GeometricShapeFactory représentée ici définit une méthode usine (Factory) appelée createShape() :

package com.example.programmingas3.geometricshapes  
{ 
    public class GeometricShapeFactory  
    { 
        public static var currentShape:IGeometricShape; 
 
        public static function createShape(shapeName:String,  
                                        len:Number):IGeometricShape 
        { 
            switch (shapeName) 
            { 
                case "Triangle": 
                    return new EquilateralTriangle(len); 
 
                case "Square": 
                    return new Square(len); 
 
                case "Circle": 
                    return new Circle(len); 
            } 
            return null; 
        } 
     
        public static function describeShape(shapeType:String, shapeSize:Number):String 
        { 
            GeometricShapeFactory.currentShape = 
                GeometricShapeFactory.createShape(shapeType, shapeSize); 
            return GeometricShapeFactory.currentShape.describe(); 
        } 
    } 
}

La méthode usine (Factory) createShape() laisse aux constructeurs de la sous-classe de la forme le soin de définir les détails des occurrences qu’ils créent, tout en renvoyant les nouveaux objets comme occurrences de IgeometricShape, ce qui permet à l’application de les traiter de façon plus générale.

La méthode describeShape() de l’exemple précédent montre comment une application peut utiliser la méthode usine (Factory) pour obtenir une référence générique à un objet spécifique. L’application peut obtenir la description d’un objet Circle nouvellement créé en procédant comme suit :

GeometricShapeFactory.describeShape("Circle", 100);

La méthode describeShape() appelle alors la méthode usine (Factory) createShape() avec les mêmes paramètres. Elle met le nouvel objet Circle dans une variable statique appelée currentShape, dont le type a été défini comme objet de IGeometricShape. La méthode describe() est ensuite appelée pour l’objet currentShape et cet appel est automatiquement résolu pour exécuter la méthode Circle.describe() qui renvoie une description détaillée du cercle.

Amélioration de l’application d’exemple

La puissance des interfaces et de l’héritage devient évidente dès qu’il s’agit de modifier l’application.

Supposons que nous voulions ajouter une nouvelle forme, un pentagone, à cette application. Il suffit de créer une classe, Pentagon, qui étend la classe RegularPolygon et définit ses propres versions des méthodes getArea() et describe(). Une nouvelle option Pentagon est ensuite ajoutée dans l’interface utilisateur de l’application. C’est tout. La classe Pentagon obtiendra automatiquement, par héritage, les fonctionnalités des méthodes getPerimeter() et getSumOfAngles() de la classe RegularPolygon. Et puisqu’elle hérite d’une classe qui implémente l’interface IGeometricShape, une occurrence de Pentagon sera également traitée comme une occurrence de IGeometricShape. Autrement dit, il n’est pas nécessaire de modifier l’une des méthodes de la classe GeometricShapeFactory, ce qui rend beaucoup plus facile l’ajout ultérieur de nouveaux types de forme. Par conséquent, il n’est pas nécessaire non plus de modifier le code qui utilise la classe GeometricShapeFactory.

A titre d’exercice, il est conseillé d’ajouter une classe Pentagon à l’exemple Geometric Shapes, afin de constater à quel point les interfaces et l’héritage facilitent le processus d’ajout de nouvelles fonctionnalités à une application.