Voorbeeld: GeometricShapes

Met de voorbeeldtoepassing GeometricShapes wordt getoond hoe een aantal objectgeoriënteerde concepten en functies met ActionScript 3.0 kunnen worden toegepast, waaronder:

  • Klassen definiëren

  • Klassen uitbreiden

  • Polymorfisme en het trefwoord override

  • Interfaces definiëren, uitbreiden en implementeren

Het bevat ook een ‘fabrieksmethode’ die klasseninstanties maakt, waarmee wordt getoond hoe een geretourneerde waarde moet worden gedeclareerd als een instantie van een interface en hoe het geretourneerde object op een algemene manier wordt gebruikt.

Zie www.adobe.com/go/learn_programmingAS3samples_flash_nl als u de toepassingsbestanden voor dit voorbeeld wilt downloaden. De toepassingsbestanden van GeometricShapes bevinden zich in de map Samples/GeometricShapes. De toepassing bestaat uit de volgende bestanden:

File

Beschrijving

GeometricShapes.mxml

of

GeometricShapes.fla

Het hoofdtoepassingsbestand in Flash (FLA) of Flex (MXML).

com/example/programmingas3/geometricshapes/IGeometricShape.as

De basisinterface die de methoden definieert die door alle toepassingsklassen van GeometricShapes moeten worden geïmplementeerd.

com/example/programmingas3/geometricshapes/IPolygon.as

Een interface die de methoden definieert die moeten worden geïmplementeerd door de GeometricShapes-toepassingsklassen met meerdere zijden.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Een type geometrische vorm met zijden van gelijke lengte die symmetrisch rond het midden van de vorm zijn geplaatst.

com/example/programmingas3/geometricshapes/Circle.as

Een geometrische vorm die een cirkel definieert.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Een subklasse van RegularPolygon die een driehoek met zijden van gelijke lengte definieert.

com/example/programmingas3/geometricshapes/Square.as

Een subklasse van RegularPolygon die een rechthoek met vier zijden van gelijke lengte definieert.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Een klasse die een fabrieksmethode bevat voor het maken van vormen op basis van een opgegeven vormtype en grootte.

Klassen GeometricShapes definiëren

De toepassing Geometric laat de gebruiker een type geometrische vorm en een grootte opgeven. Vervolgens wordt er gereageerd met een beschrijving van de vorm, het gebied en de afstand rond zijn omtrek.

De gebruikersinterface van de toepassing is eenvoudig; deze bevat een aantal besturingselementen voor het selecteren van het vormtype, het instellen van de grootte en het weergeven van de beschrijving. Het interessante deel van deze toepassing ligt in de structuur van de klassen en de interfaces zelf.

Deze toepassing werkt met geometrische vormen, maar geeft ze niet grafisch weer.

De klassen en interfaces die de geometrische vormen in dit voorbeeld definiëren, worden in het volgende diagram getoond met de Unified Modeling Language-notatie (UML):

Voorbeeldklassen GeometricShapes

Algemeen gedrag met interfaces definiëren

Deze toepassing van GeometricShapes betreft drie typen vormen‎: cirkels, vierkanten en gelijkzijdige driehoeken. De klassentructuur van GeometricShapes begint met een zeer eenvoudige interface, IGeometricShape, die de methoden opsomt die op alle drie de vormtypen van toepassing zijn:

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

De interface definieert twee methoden: de methode getArea(), die het gebied van de vorm berekent en retourneert en de methode describe(), die een tekstbeschrijving van de vormeigenschappen samenstelt.

De afstand rond de omtrek van elke vorm moet ook worden berekend. De omtrek van een cirkel wordt echter op een unieke manier berekend, zodat het gedrag afwijkt van dat van een driehoek of vierkant. Er bestaan echter nog genoeg overeenkomsten tussen driehoeken, vierkanten en andere veelhoeken, zodat het de moeite waard is een eigen interfaceklasse voor ze te definiëren: IPolygon. De interface IPolygon is ook tamelijk eenvoudig, zoals hieronder getoond:

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

Deze interface definieert twee methoden die voor alle veelhoeken gelden: de methode getPerimeter(), waarmee de gecombineerde lengte van alle zijden wordt gemeten, en de methode getSumOfAngles(), waarmee alle binnenhoeken bij elkaar worden opgeteld.

De interface IPolygon breidt de interface IGeometricShape uit, dat betekent dat elke klasse die de interface IPolygon implementeert, alle vier methoden moet declareren; de twee methoden van de interface IGeometricShape en de twee methoden van de interface IPolygon.

Klassen Shape definiëren

Zodra u een goed idee hebt van de methoden die op elk vormtype van toepassing zijn, kunt u de klassen Shape zelf definiëren. Als u wilt weten hoeveel methoden u moet implementeren, kunt u dit baseren op de eenvoudigste vorm; de klasse Circle, zoals hieronder getoond:

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

De klasse Circle implementeert de interface IGeometricShape, daarom moet deze code bieden voor zowel de methode getArea() als de methode describe(). Bovendien definieert deze de methode getCircumference(), uniek voor de klasse Circle. De klasse Circle declareert ook een eigenschap, diameter, die niet in de andere veelhoekklassen voorkomt.

De andere twee typen vormen, vierkanten en gelijkzijdige driehoeken, hebben enkele andere zaken gemeen: ze hebben allebei zijden van gelijke lengte en er zijn formules die u voor beide kunt gebruiken om de omtrek en de som van de binnenhoeken te berekenen. In feite zijn deze algemene formules van toepassing op alle normale veelhoeken die u in de toekomst zult definiëren.

De klasse RegularPolygon wordt de superklasse voor de klasse Square en de klasse EquilateralTriangle. Met een superklasse kunt u algemene methoden op één plaats definiëren, zodat u ze niet afzonderlijk in elke subklasse hoeft te definiëren. Hier is de code voor de klasse 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; 
        } 
    } 
}

Eerst declareert de klasse RegularPolygon twee eigenschappen die gemeenschappelijk zijn voor alle regelmatige veelhoeken: de lengte van elke zijde (de eigenschap sideLength) en het aantal zijden (de eigenschap numSides).

De klasse RegularPolygon implementeert de interface IPolygon en declareert alle vier de interfacemethoden van IPolygon. Twee van de methoden (de methoden getPerimeter() en getSumOfAngles()) worden met algemene formules geïmplementeerd.

Omdat de formule voor de methode getArea() verschilt van vorm tot vorm, kan de basisklassenversie van de methode niet de algemene logica opnemen die kan worden overgeërfd door de methoden van de subklassen. In plaats hiervan wordt de standaardwaarde 0 geretourneerd om aan te geven dat het gebied niet is berekend. Als u het gebied van elke vorm correct wilt berekenen, moeten de subklassen van de klasse RegularPolygon de methode getArea() zelf overschrijven.

De volgende code voor de klasse EquilateralTriangle toont hoe de methode getArea() is overschreven:

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

Het trefwoord override geeft aan dat de methode EquilateralTriangle.getArea() met opzet de methode getArea() van de superklasse RegularPolygon overschrijft. Wanneer de methode EquilateralTriangle.getArea() wordt aangeroepen, berekent deze het gebied met gebruik van de formule in de vorige code en wordt de code in de methode RegularPolygon.getArea() nooit uitgevoerd.

De klasse EquilateralTriangle daarentegen definieert zijn eigen versie van de methode getPerimeter() niet. Wanneer de methode EquilateralTriangle.getPerimeter() wordt aangeroepen, doorloopt de aanroep de overervingsketen naar boven en voert de code in de methode getPerimeter() van de superklasse RegularPolygon uit.

De constructor EquilateralTriangle() gebruikt de instructie super() om expliciet de constructor RegularPolygon() van de superklasse aan te roepen. Als beide constructors dezelfde set parameters hebben, kunt u de constructor EquilateralTriangle() weglaten en zou de constructor RegularPolygon() worden uitgevoerd. De constructor RegularPolygon() heeft echter een extra parameter numSides nodig. De constructor EquilateralTriangle() roept dus super(len, 3) aan, die de invoerparameter len en de waarde 3 doorloopt om aan te geven dat de driehoek drie zijden heeft.

De methode describe() gebruikt ook de instructie super(), maar op een andere manier. Deze gebruikt deze instructie voor het aanroepen van de versie van de superklasse RegularPolygon van de methode describe(). De methode EquilateralTriangle.describe() stelt eerst de tekenreeksvariabele desc in op een instructie over het vormtype. Vervolgens haalt de methode het resultaat van de methode RegularPolygon.describe() op door super.describe() aan te roepen; het resultaat wordt vervolgens toegevoegd aan de tekenreeks desc.

De klasse Square wordt hier niet uitgebreid besproken, maar is vergelijkbaar met de klasse EquilateralTriangle, die een constructor en eigen implementaties van de methoden getArea() en describe() biedt.

Polymorfisme en de fabrieksmethode

Een set klassen die goed gebruikmaakt van interfaces en overerving, kan op veel manieren worden gebruikt. Alle klassen die bijvoorbeeld tot nu toe zijn beschreven, implementeren ofwel de interface IGeometricShape of breiden een superklasse uit die dit doet. Als u dus een variabele definieert als instantie van IGeometricShape, hoeft u niet te weten of deze een instantie van de klasse Circle of Square is om de methode describe() aan te roepen.

De volgende code toont hoe dit werkt:

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

Wanneer myShape.describe() wordt aangeroepen, wordt de methode Circle.describe() uitgevoerd omdat Circle de onderliggende klasse is, zelfs als de variabele is gedefinieerd als een instantie van de interface IGeometricShape.

In dit voorbeeld ziet u het begrip polymorfisme in actie: exact dezelfde methodeaanroep resulteert in de uitvoering van andere code, afhankelijk van de klasse van het object waarvan de methode wordt aangeroepen.

De toepassing GeometricShapes past de op interface gebaseerde polymorfisme toe met gebruik van een vereenvoudigde versie van een ontwerppatroon, bekend als de fabrieksmethode. De term fabrieksmethode verwijst naar een functie die een object retourneert waarvan het onderliggende gegevenstype of inhoud verschilt, afhankelijk van de context.

De klasse GeometricShapeFactory die hier wordt getoond, definieert een fabrieksmethode met de naam 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(); 
        } 
    } 
}

De fabrieksmethode createShape() laat de constructors van de vormsubklasse de details definiëren van de instanties die ze maken, terwijl deze de nieuwe objecten als instanties IGeometricShape retourneert zodat ze op een algemenere manier door de toepassing kunnen worden verwerkt.

De methode describeShape() in het vorige voorbeeld toont hoe een toepassing de fabrieksmethode kan gebruiken om een algemene verwijzing op te halen naar een meer specifiek object. De toepassing kan de beschrijving voor een pas gemaakt object Circle ophalen, zoals:

GeometricShapeFactory.describeShape("Circle", 100);

De methode describeShape() roept vervolgens de fabrieksmethode createShape() met dezelfde parameters aan en slaat het nieuwe object Circle op in een statische variabele met de naam currentShape, die als een object IGeometricShape is ingevoerd. Vervolgens wordt de methode describe() aangeroepen voor het object currentShape en die aanroep wordt automatisch omgezet om de methode Circle.describe() uit te voeren, waardoor een gedetailleerde beschrijving van de cirkel wordt geretourneerd.

Voorbeeldtoepassing verbeteren

De echte kracht van interfaces en overerving wordt zichtbaar wanneer u de toepassing verbetert of wijzigt.

Stel dat u een nieuwe vorm, een vijfhoek, aan deze voorbeeldtoepassing wilt toevoegen. U moet dan een klasse Pentagon maken die de klasse RegularPolygon uitbreidt en zijn eigen versies van de methode getArea() en describe() definieert. Vervolgens moet u een nieuwe Pentagon-optie toevoegen aan de keuzelijst in de gebruikersinterface van de toepassing. Meer hoeft u niet te doen. De klasse Pentagon krijgt door overerving automatisch de functionaliteit van de methoden getPerimeter() en getSumOfAngles() van de klasse RegularPolygon. Omdat er wordt overgeërfd van een klasse die de interface IGeometricShape implementeert, kan een instantie Pentagon ook als een instantie IGeometricShape worden behandeld. Dat betekent dat als u een nieuw type vorm wilt toevoegen, u de methodehandtekening van de methoden in de klasse GeometricShapeFactory niet hoeft te wijzigen (en dus ook geen code hoeft te wijzigen die de klasse GeometricShapeFactory gebruikt).

Voeg ter oefening een klasse Pentagon aan het voorbeeld GeometricShapes toe om te zien hoe interfaces en overerving de werkbelasting van het toevoegen van nieuwe functies aan een toepassing kunnen verminderen.