Esempio: GeometricShapes

L'applicazione di esempio GeometricShapes illustra come è possibile applicare una serie di concetti e funzioni orientati agli oggetti utilizzando ActionScript 3.0, ad esempio:

  • Definizione di classi

  • Estensione di classi

  • Polimorfismo e parola chiave override

  • Definizione, estensione e implementazione delle interfacce

L'esempio include inoltre un “metodo factory” per la creazione di istanze di classi che illustra come dichiarare un valore restituito come un'istanza di un'interfaccia e utilizzare tale oggetto restituito in maniera generica.

Per ottenere i file dell'applicazione per questo esempio, vedete www.adobe.com/go/learn_programmingAS3samples_flash_it. I file dell'applicazione GeometricShapes si trovano nella cartella Samples/GeometricShapes. L'applicazione è composta dai seguenti file:

File

Descrizione

GeometricShapes.mxml

oppure

GeometricShapes.fla

Il file principale dell'applicazione in Flash (FLA) o Flex (MXML)

com/example/programmingas3/geometricshapes/IGeometricShape.as

Metodi di definizione interfaccia di base da implementare in tutte le classi dell'applicazione GeometricShapes.

com/example/programmingas3/geometricshapes/IPolygon.as

Metodo di definizione di interfaccia da implementare nelle classi dell'applicazione GeometricShapes che presentano più lati.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Tipo di figura geometrica che presenta lati di uguale lunghezza posizionati simmetricamente attorno al centro della figura.

com/example/programmingas3/geometricshapes/Circle.as

Tipo di figura geometrica che definisce un cerchio.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Sottoclasse di RegularPolygon che definisce un triangolo con lati di pari lunghezza.

com/example/programmingas3/geometricshapes/IPolygon.as

Sottoclasse di RegularPolygon che definisce un rettangolo con quattro lati di pari lunghezza.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Classe contenente un metodo factory per la creazione di figure geometriche a partire da una forma e da una dimensione specificate.

Definizione delle classi GeometricShapes

L'applicazione GeometricShapes consente di specificare un tipo di figura geometrica e una dimensione. Viene quindi restituita una descrizione della figura, la relativa area e il perimetro.

L'interfaccia utente dell'applicazione è molto semplice; essa include alcuni comandi per la selezione del tipo di figura geometrica, l'impostazione delle dimensione e la visualizzazione della descrizione. La parte più interessante di questa applicazione sta sotto la superficie, nella struttura delle classi e delle interfacce stesse.

Si tratta di un'applicazione che si occupa di figure geometriche pur senza visualizzarle graficamente.

Le classi e le interfacce che definiscono le figure geometriche di questo esempio sono visualizzate nel diagramma seguente utilizzando la notazione del linguaggio UML (Unified Modeling Language):

Classi di esempio GeometricShapes

Definizione di comportamenti comuni alle interfacce

L’applicazione GeometricShapes tratta tre diversi tipi di figure geometriche: cerchio, quadrato e triangolo equilatero. La struttura della classe GeometricShapes inizia con un'interfaccia molto semplice, IGeometricShape, che elenca una serie di metodi comuni a tutte e tre le figure geometriche:

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

L’interfaccia definisce due metodi: il metodo getArea(), che calcola e restituisce l’area della figura geometrica, e il metodo describe(), che assembla una descrizione in formato testo delle proprietà della figura.

È opportuno inoltre conoscere il perimetro di ciascuna figura. Tuttavia, il perimetro del cerchio viene definito circonferenza e viene calcolato in modo univoco, di conseguenza, in questo caso, il comportamento differisce da quello per calcolare il perimetro di un triangolo o un quadrato. Vi sono comunque sufficienti analogie tra triangoli, quadrati e altri poligoni da consentire la definizione di una nuova classe di interfaccia esclusiva per tali figure: IPolygon. Anche l'interfaccia IPolygon è piuttosto semplice, come illustrato di seguito:

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

Questa interfaccia definisce due metodi comuni a tutti i poligoni: il metodo getPerimeter() che misura la distanza combinata di tutti i lati e il metodo getSumOfAngles() che somma tutti gli angoli interni.

L'interfaccia IPolygon estende l'interfaccia IGeometricShape, di conseguenza tutte le classi che implementano l'interfaccia IPolygon devono dichiarare tutti e quattro i metodi, i due dell'interfaccia IGeometricShape e i due dell'interfaccia IPolygon.

Definizione delle classi delle figure geometriche

Una volta a conoscenza dei metodi comuni a ogni tipo di figura, potete passare alla definizione delle classi delle figure geometriche. In termini di quantità di metodi che è necessario implementare, la figura geometrica più semplice corrisponde alla classe Circle visualizzata di seguito:

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 implementa l'interfaccia IGeometricShape, di conseguenza essa deve fornire codice sia per il metodo getArea() che per il metodo describe(). Inoltre, essa definisce il metodo getCircumference(), univoco per la classe Circle. Nella classe Circle viene infine dichiarata una proprietà, diameter, che non si trova nelle altre classi dei poligoni.

Gli altri due tipi di due figure geometriche, quadrato e triangolo equilatero, presentano altre somiglianze: entrambe hanno lati di pari lunghezza e vi sono formule comuni utilizzabili per calcolarne il perimetro e la somma degli angoli interni. In realtà, tali formule comuni sono applicabili a qualsiasi altro poligono regolare definito in futuro.

La classe RegularPolygon è la superclasse sia della classe Square che della classe EquilateralTriangle. Una superclasse consente di definire metodi comuni in una sola posizione, quindi non sarà necessario definirli separatamente in ogni sottoclasse. Segue il codice per 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; 
        } 
    } 
}

In primo luogo, la classe RegularPolygon dichiara due proprietà comuni a tutti i poligoni regolari: la lunghezza di ciascun lato (proprietà sideLength) e il numero di lati (proprietà numSides).

La classe RegularPolygon implementa l'interfaccia IPolygon e dichiara tutti e quattro i metodi dell'interfaccia IPolygon. Essa implementa inoltre due di essi (i metodi getPerimeter() e getSumOfAngles()) utilizzando formule comuni.

Poiché la formula del metodo getArea() è diversa in base al tipo di figura geometrica, la versione della classe base del metodo non può includere logica comune ereditabile dai metodi delle sottoclassi. Al contrario, restituisce semplicemente un valore 0 predefinito a indicare che l'area non è stata calcolata. Per calcolare correttamente l'area di ogni figura geometrica, le sottoclassi della classe RegularPolygon devono sostituire il metodo getArea().

Il codice seguente della classe EquilateralTriangle mostra come è possibile sostituire il metodo 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; 
        } 
    } 
}

La parola chiave override indica che il metodo EquilateralTriangle.getArea() sostituisce intenzionalmente il metodo getArea() dalla superclasse RegularPolygon. Quando viene chiamato, il metodo EquilateralTriangle.getArea() calcola l'area utilizzando la formula del codice precedente, mentre il codice del metodo RegularPolygon.getArea() non viene mai eseguito.

Al contrario, la classe EquilateralTriangle non definisce una propria versione del metodo getPerimeter(). Quando il metodo EquilateralTriangle.getPerimeter() viene chiamato, la chiamata risale la catena di ereditarietà e viene eseguito il codice del metodo getPerimeter() della superclasse RegularPolygon.

La funzione di costruzione EquilateralTriangle() impiega l'istruzione super() per chiamare esplicitamente la funzione di costruzione RegularPolygon() della sua superclasse. Se entrambe le funzioni di costruzione presentano la stessa serie di parametri, anche se la funzione EquilateralTriangle() venisse omessa, la funzione di costruzione RegularPolygon() verrebbe eseguita al suo posto. Tuttavia, la funzione di costruzione RegularPolygon() richiede un parametro aggiuntivo, numSides. Quindi, la funzione di costruzione EquilateralTriangle() chiama super(len, 3), che trasmette il parametro di input len e il valore 3 per indicare che il triangolo ha tre lati.

Anche il metodo describe() utilizza l'istruzione super(), ma in una maniera differente, cioè per chiamare la versione della superclasse RegularPolygon del metodo describe(). Il metodo EquilateralTriangle.describe() imposta in primo luogo la variabile di stringa desc in modo che indichi un'istruzione relativa al tipo di figura. Quindi, ottiene il risultato del metodo RegularPolygon.describe() chiamando super.describe() e aggiunge tale risultato alla stringa desc.

La classe Square non viene descritta dettagliatamente in questa sezione, tuttavia essa è simile alla classe EquilateralTriangle, in quanto fornisce una funzione di costruzione e modalità proprie di implementazione dei metodi getArea() e describe().

Polimorfismo e metodo factory

Una serie di classi che impiega in modo proficuo interfacce ed ereditarietà può essere utilizzata in tanti modi interessanti. Ad esempio, tutte le classi delle figure geometriche descritte in precedenza o implementano l'interfaccia IGeometricShape o estendono una superclasse che lo fa. Di conseguenza, se definite una variabile come istanza di IGeometricShape, non dovete sapere se si tratta di un'istanza della classe Circle o della classe Square per chiamare il suo metodo describe().

Il codice seguente illustra come viene applicato questo principio:

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

Quando myShape.describe() viene chiamata, essa esegue il metodo Circle.describe() perché anche se la variabile è definita come un'istanza dell'interfaccia IGeometricShape, Circle è la sua classe sottostante.

L’esempio seguente mostra il principio del polimorfismo in azione: l’esatta chiamata di uno stesso metodo porta all’esecuzione di codici differenti, a seconda della classe dell’oggetto il cui metodo viene chiamato.

L'applicazione GeometricShapes applica questo tipo di polimorfismo basato sulle interfacce impiegando una versione semplificata di un modello di progettazione conosciuto come metodo factory. Il termine metodo factory indica una funzione che restituisce un oggetto il cui tipo di dati o contenuto sottostante può differire in base al contesto.

La classe GeometricShapeFactory illustrata qui definisce un metodo factory denominato 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(); 
        } 
    } 
}

Il metodo factory createShape() consente alle funzioni di costruzione delle sottoclassi delle figure geometriche di definire i dettagli delle istanze da esse create, ma di restituire i nuovi oggetti come istanze di IGeometricShape, in modo che possano essere gestiti dall'applicazione in maniera più generica.

Il metodo describeShape() dell'esempio precedente mostra come un'applicazione può utilizzare il metodo factory per ottenere un riferimento generico a un oggetto più specifico. L'applicazione è in grado di ottenere la descrizione di un oggetto Circle appena creato come segue:

GeometricShapeFactory.describeShape("Circle", 100);

Il metodo describeShape() chiama quindi il metodo factory createShape() con gli stessi parametri, memorizzando il nuovo oggetto Circle in una variabile statica denominata currentShape, originariamente inserita come un oggetto di IGeometricShape. Quindi, viene chiamato il metodo describe() sull'oggetto currentShape e la chiamata viene automaticamente risolta per eseguire il metodo Circle.describe(), che restituisce una descrizione dettagliata del cerchio.

Ottimizzazione dell'applicazione di esempio

La reale portata di interfacce ed ereditarietà diventa evidente quando un'applicazione viene ottimizzata o modificata.

Supponete di dover aggiungere una nuova figura geometrica, un pentagono, all'applicazione di esempio. Verrebbe creata una classe Pentagon che estende la classe RegularPolygon e definisce versioni proprie dei metodi getArea() e describe(). Quindi verrebbe aggiunta una nuova opzione Pentagon alla casella combinata dell'interfaccia utente dell'applicazione. Niente altro. La classe Pentagon otterrebbe automaticamente le funzionalità del metodo getPerimeter() e del metodo getSumOfAngles() ereditandole dalla classe RegularPolygon. Poiché eredita da una classe che implementa l'interfaccia IGeometricShape, un'istanza di Pentagon può essere considerata anche un'istanza di IGeometricShape. In pratica ogni volta che desiderate aggiungere un nuovo tipo di figura geometrica, non sarà necessario modificare la firma di nessuno dei metodi della classe GeometricShapeFactory (e quindi non sarà necessario modificare nemmeno i codici che utilizzano la classe GeometricShapeFactory).

Potete aggiungere una classe Pentagon all'esempio Geometric Shapes come esercizio, per vedere come le interfacce e l'ereditarietà possono facilitare le operazioni di aggiunta di nuove funzioni a un'applicazione.