Beispiel: GeometricShapes

Die Beispielanwendung „GeometricShapes“ verdeutlicht, wie verschiedene objektorientierte Konzepte und Funktionen mit ActionScript 3.0 angewendet werden können:

  • Definieren von Klassen

  • Erweitern von Klassen

  • Polymorphismus und das Schlüsselwort override

  • Definieren, Erweitern und Implementieren von Schnittstellen

Die Beispielanwendung enthält darüber hinaus eine „Factory-Methode“, mit der gezeigt wird, wie ein Rückgabewert als Instanz einer Schnittstelle deklariert und das zurückgegebene Objekt generisch verwendet wird.

Die Anwendungsdateien für dieses Beispiel finden Sie auf www.adobe.com/go/learn_programmingAS3samples_flash_de. Die Dateien der Anwendung „GeometricShapes“ befinden sich im Ordner „Samples/GeometricShapes“. Die Anwendung umfasst die folgenden Dateien:

Datei

Beschreibung

GeometricShapes.mxml

oder

GeometricShapes.fla

Die Hauptanwendungsdatei im Flash-Format (FLA) oder Flex-Format (MXML).

com/example/programmingas3/geometricshapes/IGeometricShape.as

Die Methoden, mit denen die grundlegenden Schnittstelle definiert wird, die von allen GeometricShapes-Anwendungsklassen implementiert werden müssen.

com/example/programmingas3/geometricshapes/IPolygon.as

Die Methoden, mit denen eine Schnittstelle definiert wird, die von allen GeometricShapes-Anwendungsklassen mit mehreren Seiten implementiert werden müssen.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Eine geometrische Form, deren gleich lange Seiten symmetrisch um den Mittelpunkt der Form angeordnet sind.

com/example/programmingas3/geometricshapes/Circle.as

Eine geometrische Form, die einen Kreis definiert.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Eine Unterklasse von RegularPolygon, die ein Dreieck definiert, dessen Seiten die gleiche Länge aufweisen.

com/example/programmingas3/geometricshapes/Square.as

Eine Unterklasse von RegularPolygon, die ein Rechteck definiert, dessen vier Seiten die gleiche Länge aufweisen.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Eine Klasse, die eine Factory-Methode zum Erstellen von Formen eines bestimmten Typs und einer bestimmten Größe enthält.

Definieren der GeometricShapes-Klassen

Mit der Anwendung „GeometricShapes“ kann ein Benutzer Typ und Größe einer geometrischen Form angeben. Die Anwendung reagiert dann mit einer Beschreibung der Form, ihrer Fläche und ihres Umfangs.

Die Benutzerschnittstelle der Anwendung ist einfach aufgebaut und enthält nur wenige Steuerelemente zur Auswahl von Typ und Größe der Form sowie zum Anzeigen der Beschreibung. Der interessanteste Teil dieser Anwendung liegt unter der Oberfläche, in der Struktur der Klassen und Schnittstellen selbst.

Die Anwendung arbeitet zwar mit geometrischen Formen, zeigt sie aber nicht grafisch an.

Im folgenden Diagramm sind die Klassen und Schnittstellen, mit denen die geometrischen Formen in diesem Beispiel definiert werden, in der Unified Modeling Language-Notation (UML) dargestellt:

GeometricShapes-Beispielklassen

Definieren des allgemeinen Verhaltens mit Schnittstellen

In dieser GeometricShapes-Anwendung werden drei Arten von Formen verwendet: Kreise, Quadrate und gleichseitige Dreiecke. Die GeometricShapes-Klassenstruktur beginnt mit einer sehr einfachen Schnittstelle, IGeometricShape, in der die Methoden aufgeführt sind, die für alle drei Formen gelten:

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

Die Schnittstelle definiert zwei Methoden: die Methode getArea(), mit der die Fläche der Form berechnet und zurückgegeben wird, sowie die Methode describe(), die eine Textbeschreibung der Eigenschaften der Form zusammensetzt.

Außerdem soll der Umfang jeder Form ermittelt werden. Jedoch wird der Umfang eines Kreises als Kreisumfang bezeichnet und mit einem besonderen Verfahren berechnet. Daher weicht das Verhalten von dem für ein Dreieck oder ein Quadrat ab. Dennoch gibt es ausreichend Gemeinsamkeiten zwischen Dreiecken, Quadraten und anderen Polygonen, dass es sinnvoll ist, eine neue Schnittstellenklasse für sie zu definieren: IPolygon. Die IPolygon-Schnittstelle ist ebenfalls recht einfach:

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

Diese Schnittstelle definiert zwei Methoden, die alle Polygone gemeinsam haben: die getPerimeter()-Methode, die den kombinierten Abstand aller Seiten misst, und die getSumOfAngles()-Methode, die alle Innenwinkel summiert.

Die IPolygon-Schnittstelle erweitert die IGeometricShape-Schnittstelle. Dies bedeutet, dass alle Klassen, welche die IPolygon-Schnittstelle implementieren, alle vier Methoden deklarieren müssen – die zwei aus der IGeometricShape-Schnittstelle und die zwei aus der IPolygon-Schnittstelle.

Definieren der Formklassen

Jetzt, nachdem Sie eine Vorstellung der Methoden haben, die für jeden Formtyp gleich sind, können Sie die Formklassen selbst definieren. Hinsichtlich der Anzahl der zu implementierenden Methoden ist die einfachste Form die im Folgenden aufgeführte Circle-Klasse:

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; 
        } 
    } 
}

Die Circle-Klasse implementiert die IGeometricShape-Schnittstelle, also müssen Sie einen Code für die Methoden getArea() und describe() bereitstellen. Darüber hinaus definiert sie die getCircumference()-Methode, die nur für die Circle-Klasse gilt. Die Circle-Klasse deklariert ebenfalls eine Eigenschaft, diameter, die in den anderen Polygonklassen nicht vorhanden sein wird.

Die anderen beiden Formtypen, Quadrate und gleichseitige Dreiecke, haben einige andere Gemeinsamkeiten: Bei beiden Typen sind die Seiten gleich lang und für beide können die gleichen Formeln zum Berechnen des Umfangs und der Summe der Innenwinkel verwendet werden. Tatsächlich gelten diese gemeinsamen Formeln auch für alle anderen regelmäßigen Polygone, die Sie noch definieren werden.

Die RegularPolygon-Klasse ist die übergeordnete Klasse der Square-Klasse und der EquilateralTriangle-Klasse. Mit einer übergeordneten Klasse können Sie allgemeine Methoden an einem Ort definieren, sodass Sie sie nicht in jeder untergeordneten Klasse erneut definieren müssen. Im Folgenden finden Sie den Code für die RegularPolygon-Klasse:

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; 
        } 
    } 
}

Zuerst deklariert die RegularPolygon-Klasse zwei Eigenschaften, die alle regelmäßigen Polygone gemeinsam haben: die Länge jeder Seite (Eigenschaft sideLength) und die Anzahl der Seiten (Eigenschaft numSides).

Die RegularPolygon-Klasse implementiert die IPolygon-Schnittstelle und deklariert alle vier IPolygon-Schnittstellenmethoden. Sie implementiert zwei dieser Methoden (getPerimeter() und getSumOfAngles()) mithilfe gemeinsamer Formeln.

Da die Formel der getArea()-Methode je nach Form anders ist, kann die Basisklassenversion der Methode keine gemeinsame Logik enthalten, die von den Methoden der Unterklasse geerbt werden kann. Stattdessen gibt sie einfach eine 0 als Standardwert zurück und kennzeichnet so, dass die Fläche nicht berechnet wurde. Um die Fläche jeder Form korrekt zu berechnen, müssen die Unterklassen der RegularPolygon-Klasse die getArea()-Methode selbst übergehen.

Der folgende Code für die EquilateralTriangle-Klasse zeigt, wie die getArea()-Methode überschrieben wird:

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; 
        } 
    } 
}

Das Schlüsselwort override gibt an, dass die EquilateralTriangle.getArea()-Methode die getArea()-Methode der RegularPolygon-Unterklasse absichtlich überschreibt. Wenn die EquilateralTriangle.getArea()-Methode aufgerufen wird, berechnet sie die Fläche mithilfe der Formel im vorangegangenen Code, und der Code in der RegularPolygon.getArea()-Methode wird nie ausgeführt.

Im Gegensatz dazu definiert die EquilateralTriangle-Klasse keine eigene Version der getPerimeter()-Methode. Wenn die EquilateralTriangle.getPerimeter()-Methode aufgerufen wird, durchläuft der Aufruf die Vererbungskette aufwärts und führt den Code in der getPerimeter()-Methode der übergeordneten RegularPolygon-Klasse aus.

Der EquilateralTriangle()-Konstruktor verwendet die super()-Anweisung, um den RegularPolygon()-Konstruktor der übergeordneten Klasse explizit aufzurufen. Wenn beide Konstruktoren über den gleichen Parametersatz verfügen, können Sie den EquilateralTriangle()-Konstruktor weglassen. Stattdessen wird der RegularPolygon()-Konstruktor ausgeführt. Der RegularPolygon()-Konstruktor benötigt jedoch einen zusätzlichen Parameter, numSides. Somit ruft der EquilateralTriangle()-Konstruktor super(len, 3) auf, der den Eingabeparameter len und den Wert 3 übergibt. Auf diese Weise wird angegeben, dass das Dreieck drei Seiten hat.

Die describe()-Methode verwendet ebenfalls die super()-Anweisung, jedoch auf andere Weise. Sie ruft damit die Version der RegularPolygon-Superklasse der describe()-Methode auf. Die EquilateralTriangle.describe()-Methode stellt zuerst die String-Variable desc auf eine Anweisung zum Typ der Form ein. Dann ruft sie die Ergebnisse der RegularPolygon.describe()-Methode ab, indem sie super.describe() aufruft, und hängt das Ergebnis an den String desc an.

Die Square-Klasse wird hier nicht in allen Einzelheiten besprochen, aber sie ähnelt der EquilateralTriangle-Klasse. Sie stellt einen Konstruktor und ihre eigenen Implementationen der Methoden getArea() und describe() bereit.

Polymorphismus und die Factory-Methode

Ein Klassensatz, der Schnittstellen und Vererbung einsetzt, kann auf viele interessante Arten verwendet werden. Beispielsweise implementieren alle bisher beschriebenen Formklassen entweder die IGeometricShape-Schnittstelle oder erweitern eine Unterklasse, die diese Schnittstelle implementiert. Wenn Sie also eine Variable als Instanz von IGeometricShape definieren, müssen Sie nicht unbedingt wissen, ob es sich nun um eine Instanz der Circle- oder der Square-Klasse handelt, wenn Sie nur ihre describe()-Methode aufrufen möchten.

Der folgende Code zeigt, wie dies funktioniert:

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

Wenn myShape.describe() aufgerufen wird, führt es die Methode Circle.describe() aus, denn obwohl die Variable als Instanz der IGeometricShape-Schnittstelle definiert ist, ist „Circle“ die zugrunde liegende Klasse.

Dieses Beispiel veranschaulicht die Funktionsweise von Polymorphismus: Der exakt gleiche Methodenaufruf führt zur Ausführung von unterschiedlichem Code, je nachdem, welcher Klasse das Objekt angehört, dessen Methode aufgerufen wird.

Die Anwendung „GeometricShapes“ wendet diese Art des schnittstellenbasierten Polymorphismus mit einer einfachen Version eines Entwurfsmusters an, das als Factory-Methode bezeichnet wird. Der Begriff Factory-Methode bezieht sich auf eine Funktion, die ein Objekt zurückgibt, dessen zugrunde liegender Datentyp oder Inhalt je nach Kontext unterschiedlich sein kann.

Die hier vorgestellte GeometricShapeFactory-Klasse definiert eine Factory-Methode namens 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(); 
        } 
    } 
}

Die Factory-Methode createShape() ermöglicht es den Konstruktoren der Form-Unterklasse, die Details der von ihnen erstellten Instanzen zu definieren, während die neuen Objekte als IGeometricShape-Instanzen zurückgegeben werden, sodass sie von der Anwendung allgemeiner verarbeitet werden können.

Die describeShape()-Methode aus dem vorangegangenen Beispiel zeigt, wie eine Anwendung die Factory-Methode verwenden kann, um einen generischen Verweis auf ein spezifischeres Objekts zu erhalten. Die Anwendung kann die Beschreibung für das neu erstellte Circle-Objekt wie folgt erhalten:

GeometricShapeFactory.describeShape("Circle", 100);

Die describeShape()-Methode ruft dann die Factory-Methode createShape() mit den gleichen Parametern auf und speichert das neue Circle-Objekt in einer statischen Variablen namens currentShape, die als ein IGeometricShape-Objekt typisiert wurde. Als Nächstes wird die describe()-Methode für das currentShape-Objekt aufgerufen. Dieser Aufruf wird automatisch aufgelöst, um die Circle.describe()-Methode auszuführen, die eine detaillierte Beschreibung des Kreises zurückgibt.

Ausbauen der Beispielanwendung

Die wahre Leistungsfähigkeit von Schnittstellen und Vererbung wird sichtbar, wenn Sie Ihre Anwendung aufwerten oder ändern.

Angenommen, Sie möchten der Beispielanwendung eine neue Form, ein Fünfeck, hinzufügen. In diesem Fall erstellen Sie eine Pentagon-Klasse, die die RegularPolygon-Klasse erweitert und ihre eigene Version der Methoden getArea() und describe() definiert. Dann würden Sie dem Kombinationsfeld in der Benutzeroberfläche der Anwendung eine neue Pentagon-Option hinzufügen. Das ist dann aber auch schon alles. Die Pentagon-Klasse würde automatisch die Funktionen der Methoden getPerimeter() und getSumOfAngles() der RegularPolygon-Klasse erben. Da sie von einer Klasse erbt, die bereit die IGeometricShape-Schnittstelle geerbt hat, kann eine Pentagon-Instanz ebenfalls als eine IGeometricShape-Instanz behandelt werden. Dies bedeutet, dass es zum Hinzufügen eines neuen Formtyps nicht erforderlich ist, die Methodensignatur einer der Methoden in der GeometricShapeFactory-Klasse zu ändern (folglich müssen auch keine Änderungen an Code vorgenommen werden, der die GeometricShapeFactory-Klasse verwendet).

Sie können dem GeometricShapes-Beispiel jetzt zur Übung eine Pentagon-Klasse hinzufügen. Dabei werden Sie feststellen, wie Schnittstellen und Vererbung das Hinzufügen von neuen Funktionen zu einer Anwendung vereinfachen.