Przykład: GeometricShapes

Przykładowa aplikacja GeometricShapes ilustruje szereg pojęć i mechanizmów programowania zorientowanego obiektowo w języku ActionScript 3.0, a w szczególności:

  • Definiowanie klas.

  • Rozszerzanie klas.

  • Polimorfizm i użycie słowa kluczowego override .

  • Definiowanie, rozszerzanie i implementowanie interfejsów.

Zawiera także „metodę fabrykującą”, która tworzy instancje klas, i demonstruje deklarowanie wartości zwracanej jako instancji interfejsu oraz uniwersalne traktowanie zwróconego w ten sposób obiektu.

Aby pobrać pliki tej przykładowej aplikacji, należy przejść na stronę www.adobe.com/go/learn_programmingAS3samples_flash_pl . Pliki aplikacji GeometricShapes znajdują się w folderze Samples/GeometricShapes. Aplikacja składa się z następujących plików:

File

Opis

GeometricShapes.mxml

lub

GeometricShapes.fla

Główny plik aplikacji w formacie Flash (FLA) lub Flex (MXML).

com/example/programmingas3/geometricshapes/IGeometricShape.as

Interfejs bazowy definiujący metody, które powinny być implementowane przez klasy aplikacji GeometricShapes.

com/example/programmingas3/geometricshapes/IPolygon.as

Interfejs definiujący metody, które powinny być implementowane przez klasy aplikacji GeometricShapes dla wieloboków.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Typ kształtu geometrycznego, który ma boki równej długości rozmieszczone symetrycznie wokół środka kształtu.

com/example/programmingas3/geometricshapes/Circle.as

Typ kształtu geometrycznego definiujący koło.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Podklasa klasy RegularPolygon definiująca trójkąt równoboczny.

com/example/programmingas3/geometricshapes/Square.as

Podklasa klasy RegularPolygon definiująca prostokąt o równych bokach.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Klasa zawierająca metodę fabrykującą kształty o zadanym typie i rozmiarze.

Definiowanie klas dla aplikacji GeometricShapes

Aplikacja GeometricShapes umożliwia użytkownikowi określenie typu i rozmiaru kształtu geometrycznego. W odpowiedzi wyświetla opis kształtu, jego pole powierzchni oraz obwód.

Interfejs użytkownika aplikacji jest trywialny i obejmuje kilka elementów sterujących do wybierania typu kształtu, określania rozmiaru i wyświetlania opisu. Najbardziej interesująca warstwa tej aplikacji pozostaje niewidoczna dla użytkownika — jest nią struktura klas i interfejsów.

Aplikacja operuje na kształtach geometrycznych, ale nie wyświetla ich graficznie.

Klasy i interfejsy, które definiują kształty geometryczne w tym przykładzie zostały przedstawione na następującym schemacie przy użyciu notacji UML (Unified Modeling Language):

Powiększ obraz
Klasy z przykładu GeometricShapes

Definiowanie wspólnych zachowań przy użyciu interfejsów

Aplikacja GeometricShapes operuje na trzech typach kształtów: kołach, kwadratach i trójkątach równobocznych. Struktura klas aplikacji GeometricShapes zaczyna się od bardzo prostego interfejsu IGeometricShape, w którym zadeklarowane są metody wspólne dla wszystkich trzech typów kształtów:

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

Interfejs definiuje dwie metody: metodę getArea() , która oblicza i zwraca pole powierzchni kształtu, oraz metodę describe() , która komponuje opis tekstowy z właściwości kształtu.

Wskazana jest również znajomość długości obwodu każdego z kształtów. Jednak obwód koła oblicza się w nietypowy sposób, a zatem zachowanie koła odbiega od zachowania pozostałych kształtów, tj. trójkąta i kwadratu. Natomiast trójkąty, kwadraty i inne wieloboki są do siebie na tyle podobne, że celowe jest zdefiniowanie nowego interfejsu — IPolygon — specjalnie dla nich. Interfejs IPolygon także jest dość prosty:

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

Ten interfejs definiuje dwie metody wspólne dla wszystkich wieloboków: metodę getPerimeter() , która sumuje długość wszystkich boków, oraz metodę getSumOfAngles() , która sumuje wszystkie kąty wewnętrzne.

Interfejs IPolygon rozszerza interfejs IGeometricShape, co oznacza, że każda klasa implementująca interfejs IPolygon musi zawierać deklaracje wszystkich czterech metod — dwóch z interfejsu IGeometricShape i dwóch z interfejsu IPolygon.

Definiowanie klas kształtów

Po zidentyfikowaniu metod wspólnych dla wszystkich kształtów możemy przystąpić do definiowania klas samych kształtów. Pod względem liczby klas, jakie należy zaimplementować, najprostszym kształtem jest koło — klasa 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; 
        } 
    } 
}

Klasa Circle implementuje interfejs IGeometricShape, a więc musi zawierać zarówno kod metody getArea() , jak i kod metody describe() . Ponadto definiuje metodę getCircumference() występującą tylko w klasie Circle. Klasa Circle zawiera także deklarację właściwości diameter , której nie maja klasy wieloboków.

Pozostałe dwa typy kształtów — kwadraty i trójkąty równoboczne — mają kilka innych cech wspólnych: mają boki równej długości oraz wspólne wzory na obliczanie obwodu i sumy kątów wewnętrznych. W istocie te wspólne wzory będą miały zastosowanie do wszystkich innych wieloboków foremnych, które być może zdefiniujemy w przyszłości.

Klasa RegularPolygon jest nadklasą dla klasy Square i klasy EquilateralTriangle. Nadklasa pozwala na zdefiniowanie w jednym miejscu wszystkich wspólnych metod, przez co nie trzeba definiować ich osobno w każdej podklasie. Oto kod klasy 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; 
        } 
    } 
}

W klasie RegularPolygon najpierw deklarowane są dwie właściwości wspólne dla wszystkich wieloboków foremnych: długość boku (właściwość sideLength ) oraz liczba boków (właściwość numSides ).

Klasa RegularPolygon implementuje interfejs IPolygon i zawiera deklaracje wszystkich czterech metod określonych w interfejsie IPolygon. Dwie z tych metod — getPerimeter() oraz getSumOfAngles() — implementuje przy użyciu wspólnych wzorów.

Ponieważ wzór używany w metodzie getArea() jest różny w poszczególnych kształtach, wersja tej metody w klasie bazowej nie może zawierać wspólnej logiki dziedziczonej przez metody w podklasach. Dlatego metoda ta zwraca wartość domyślną 0, która oznacza, że pole powierzchni nie zostało obliczone. Aby prawidłowo obliczać pola powierzchni poszczególnych kształtów, podklasy klasy RegularPolygon muszą przesłaniać metodę getArea() .

Poniższy kod klasy EquilateralTriangle ilustruje przesłanianie metody 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; 
        } 
    } 
}

Słowo kluczowe override oznacza, że metoda EquilateralTriangle.getArea() celowo przesłania metodę getArea() z nadklasy RegularPolygon. Po wywołaniu metody EquilateralTriangle.getArea() pole powierzchni jest obliczane według wzoru zawartego w powyższym przykładzie, a kod w metodzie RegularPolygon.getArea() nigdy nie jest wykonywany.

Natomiast klasa EquilateralTriangle nie definiuje własnej wersji metody getPerimeter() . Wywołanie metody EquilateralTriangle.getPerimeter() jest przekazywane w górę w łańcuchu dziedziczenia i wykonywany jest kod metody getPerimeter() z nadklasy RegularPolygon.

W konstruktorze EquilateralTriangle() użyto instrukcji super() w celu jawnego wywołania konstruktora RegularPolygon() z nadklasy. Gdyby oba konstruktory miały ten sam zestaw parametrów, można byłoby pominąć konstruktor EquilateralTriangle() , z zamiast niego wykonywany byłby konstruktor RegularPolygon() . Jednak konstruktor RegularPolygon() potrzebuje dodatkowego parametru numSides . Dlatego konstruktor EquilateralTriangle() zawiera wywołanie super(len, ) , które przekazuje parametr wejściowy len oraz wartość 3 oznaczającą, że trójkąt ma 3 boki.

Metoda describe() również wykorzystuje instrukcję super() , ale w inny sposób. Wykorzystuje ją w celu wywołania wersji nadklasy RegularPolygon metody describe() . Metoda EquilateralTriangle.describe() najpierw przypisuje zmiennej desc typu String informację o typie kształtu. Następnie pobiera wynik metody RegularPolygon.describe() , wywołując super.describe() , po czym dołącza ten wynik do ciągu znaków desc .

Klasy Square nie będziemy tutaj szczegółowo omawiać, ale jest ona podobna do klasy EquilateralTriangle i zawiera konstruktor oraz własne implementacje metod getArea() i describe() .

Polimorfizm i metoda fabrykująca

Zestaw klas we właściwy sposób implementujących interfejsy i korzystających z dziedziczenia można zastosować na wiele różnych sposobów. Na przykład wszystkie opisane dotąd klasy kształtów albo implementują interfejs IGeometricShape, albo rozszerzają nadklasę, która implementuje ten interfejs. Jeśli zatem zdefiniujemy zmienną jako instancję IGeometricShape, to aby wywołać jej metodę describe() nie musimy wiedzieć, czy jest to faktycznie instancja klasy Circle, czy Square.

Ilustruje to poniższy przykład kodu:

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

Wywołanie myShape.describe() powoduje wykonanie metody Circle.describe() , ponieważ, choć zmienna jest zdefiniowana jako instancja interfejsu IGeometricShape, należy do klasy Circle.

Przykład ten prezentuje istotę polimorfizmu: to samo wywołanie metody powoduje wykonanie różnego kodu w zależności od tego, do jakiej klasy należy obiekt wywoływanej metody.

W aplikacji GeometricShapes zastosowano polimorfizm oparty na interfejsach przy użyciu uproszczonej wersji wzorca projektowego znanego jako wzorzec metody fabrykującej. Termin metoda fabrykująca oznacza funkcję zwracającą obiekt, którego typ danych lub zawartość może być różna w zależności od kontekstu.

W przedstawionej tutaj klasie GeometricShapeFactory zdefiniowana jest metoda fabrykująca o nazwie 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(); 
        } 
    } 
}

Metoda fabrykująca createShape() umożliwia konstruktorom podklas kształtów definiowanie szczegółów tworzonych przez nie instancji, a przy tym zwracanie nowych obiekty jako instancji interfejsu IGeometricShape, która aplikacja może traktować w sposób uniwersalny.

Metoda describeShape() w poprzednim przykładzie ilustruje zastosowanie metody fabrykującej do uzyskiwania ogólnych odwołań do ściślej sprecyzowanych obiektów. Aplikacja może uzyskać opis nowo utworzonego obiektu Circle w następujący sposób:

GeometricShapeFactory.describeShape("Circle", 100);

Następnie metoda describeShape() wywołuje metodę fabrykująca createShape() z tymi samymi parametrami i zapisuje nowy obiekt Circle w zmiennej statycznej currentShape , którą zadeklarowano jako obiekt IGeometricShape. Teraz wywoływana jest metoda describe() obiektu currentShape , a wywołanie to jest automatycznie tłumaczone na wywołanie metody Circle.describe() , która zwraca szczegółowy opis koła.

Udoskonalenia aplikacji przykładowej

Prawdziwy potencjał interfejsów i dziedziczenia ujawnia się przy udoskonalaniu lub modyfikowaniu aplikacji.

Załóżmy, że chcemy dodać do aplikacji przykładowej nowy kształt, pięciobok. Utworzylibyśmy wówczas klasę Pentagon rozszerzającą klasę RegularPolygon, z własną wersją metod getArea() i describe() . Następnie dodalibyśmy opcję Pentagon do odpowiedniego pola w interfejsie użytkownika aplikacji. I to wszystko. Klasa Pentagon automatycznie uzyskałaby funkcjonalność metod getPerimeter() oraz getSumOfAngles() w wyniku dziedziczenia z klasy RegularPolygon. Ponieważ klasa Pentagon dziedziczyłaby z klasy implementującej interfejs IGeometricShape, instancję klasy Pentagon można byłoby traktować jak instancję interfejsu IGeometricShape. Oznacza to, że aby dodać nowy typ kształtu, nie trzeba zmieniać sygnatur metod w klasie GeometricShapeFactory (a w konsekwencji nie trzeba modyfikować kodu, w którym klasa GeometricShapeFactory jest używana).

Czytelnik może w ramach samodzielnego ćwiczenia dodać klasę Pentagon do przykładowej aplikacji GeometricShapes, aby przekonać się, że interfejsy i dziedziczenie zmniejszają pracochłonność rozbudowywania aplikacji.