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, en particulier :

Cet exemple comprend aussi une « méthode de factorisation » qui crée des instances de classes, montrant ainsi comment déclarer une valeur de retour comme instance d'une interface et utiliser l'objet ainsi renvoyé de manière générique.

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

Fichier

Description

GeometricShapes.mxml

ou

GeometricShapes.fla

Le fichier d'application principal pour 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, définissant un triangle équilatéral.

com/example/programmingas3/geometricshapes/Square.as

Sous-classe de RegularPolygon, définissant un rectangle dont les quatre côtés sont égaux.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Classe contenant une « méthode de factorisation » pour créer des formes à partir d'un type et d'une taille de forme.

Sous-rubriques

Définition des classes de GeometricShapes
Définition d'un comportement commun à l'aide d'interfaces
Définition des classes de formes
Le polymorphisme et la « méthode de factorisation »
Amélioration de l'application d'exemple

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 triviale : 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. Elle représente une petite bibliothèque de classes et d'interfaces qui seront réutilisées dans un autre chapitre, dans la section Exemple : SpriteArranger. L'exemple SpriteArranger affiche les formes graphiquement et permet à l'utilisateur de les manipuler, à partir de la structure de classes fournie ici, dans l'application GeometricShapes.

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


Diagramme UML des classes de cette application : l'interface IGeometricShape est implémentée par la classe Circle et l'interface IPolygon, elle-même implémentée par la classe RegularPolygon, qu'étendent les classes EquilateralTriangle et Square

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 liste 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.

Nous voulons aussi 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 le voit :

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 longueurs égales, 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 êtes susceptible de définir par la suite.

La classe RegularPolygon sera la super-classe de la classe Square et de la classe EquilateralTriangle. Une super-classe 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 par défaut de 0 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 devront redéfinir elles-mêmes la méthode getArea().

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

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 super-classe RegularPolygon. Lorsque la méthode EquilateralTriangle.getArea() est appelée, elle calcule la surface à l'aide de la formule du code précédent, et 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 super-classe RegularPolygon.

Le constructeur EquilateralTriangle() utilise l'instruction super() pour invoquer explicitement le constructeur RegularPolygon() de sa super-classe. 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 passe le paramètre en entrée len et la valeur 3 pour indiquer que le nombre de côtés du triangle est 3.

La méthode describe() utilise également l'instruction super(), mais de façon différente, afin d'invoquer la version de la méthode describe() dans la super-classe 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 ajoute ces résultats à la fin de la chaîne desc.

La classe Square ne sera 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().

Le polymorphisme et la « méthode de factorisation »

Il est possible d'utiliser de diverses façons intéressantes un ensemble de classes utilisant à 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 super-classe qui se charge de cette implémentation. Si l'on définit une variable comme étant une instance de IGeometricShape, il n'est donc pas nécessaire, pour appeler sa méthode describe(), de savoir si c'est une instance 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 instance 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 de factorisation ». L'expression méthode de factorisation fait référence à 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 de factorisation nommé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 de factorisation createShape() laisse aux constructeurs de la sous-classe de la forme le soin de définir les détails des instances qu'ils créent, tout en renvoyant les nouveaux objets comme instances 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 de factorisation 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 de factorisation createShape() avec les mêmes paramètres, en mettant le nouvel objet Circle dans une variable statique nommé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 nouvelle 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éthode getPerimeter() et getSumOfAngles() de la classe RegularPolygon. Et puisqu'elle hérite d'une classe qui implémente l'interface IGeometricShape, une instance de Pentagon sera également traitée comme une instance 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.

À 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.


Flash CS3

 

M'envoyer un message électronique lorsque des commentaires sont ajoutés à cette page | Rapport de commentaire

Page en cours: http://livedocs.adobe.com/flash/9.0_fr/main/00000070.html