Exemplo: GeometricShapes

O aplicativo de amostra GeometricShapes mostra como vários conceitos e recursos orientados a objetos podem ser aplicados usando o ActionScript 3.0, inclusive:

  • Definição de classes

  • Extensão de classes

  • Polimorfismo e a palavra-chave override

  • Definição, extensão e implementação de interfaces

Ele também inclui um “método de fábrica” que cria ocorrências de classes, mostrando como declarar um valor de retorno como uma ocorrência de uma interface, e usar esse objeto retornado de uma maneira genérica.

Para obter os arquivos do aplicativo desta amostra, consulte www.adobe.com/go/learn_programmingAS3samples_flash_br. Os arquivos do aplicativo GeometricShapes podem ser encontrados na pasta Amostras/GeometricShapes. O aplicativo consiste nos seguintes arquivos:

Arquivo

Descrição

GeometricShapes.mxml

ou

GeometricShapes.fla

O arquivo principal do aplicativo no Flash (FLA) ou no Flex (MXML).

com/example/programmingas3/geometricshapes/IGeometricShape.as

A interface base que define métodos a serem implementados por todas as classes do aplicativo GeometricShapes.

com/example/programmingas3/geometricshapes/IPolygon.as

Uma interface que define métodos a serem implementados pelas classes do aplicativo GeometricShapes que têm vários lados.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Um tipo de forma geométrica que tem lados de comprimento igual prolongados simetricamente em torno do centro da forma.

com/example/programmingas3/geometricshapes/Circle.as

Um tipo de forma geométrica que define um círculo.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Uma subclasse de RegularPolygon que define um triângulo com todos os lados com o mesmo comprimento.

com/example/programmingas3/geometricshapes/Square.as

Uma subclasse de RegularPolygon que define um retângulo com os quatro lados com o mesmo comprimento.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Uma classe que contém um método de fábrica para criar formas com tamanho e tipo especificados.

Definição das classes GeometricShapes

O aplicativo GeometricShapes permite que o usuário especifique um tipo de forma geométrica e um tamanho. Em seguida, ele responde com uma descrição da forma, sua área e a distância em torno de seu perímetro.

A interface de usuário do aplicativo é trivial, incluindo alguns controles para seleção do tipo de forma, configuração do tamanho e exibição da descrição. A parte mais interessante desse aplicativo está sob a superfície, na estrutura das classes e das próprias interfaces.

Esse aplicativo trata de formas geométricas, mas não as exibe graficamente.

As classes e interfaces que definem as formas geométricas neste exemplo são mostradas no diagrama a seguir que usa a notação UML (Linguagem de modelação unificada):

Classes de exemplo do GeometricShapes

Definição do comportamento comum com interfaces

Este aplicativo GeometricShapes trata de três tipos de formas: círculos, quadrados e triângulos eqüiláteros. A estrutura de classe GeometricShapes começa com uma interface muito simples, IGeometricShape, que lista métodos comuns para todos os três tipos de formas:

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

A interface define dois métodos: o método getArea(), que calcula e retorna a área da forma, e o método describe(), que monta uma descrição de texto das propriedades da forma.

Também é desejável saber também a distância em torno do perímetro de cada forma. No entanto, o perímetro de um círculo é chamado de circunferência, e é calculado de uma maneira exclusiva, portanto o comportamento diverge daquele de um triângulo ou de um quadrado. Ainda há semelhança suficiente entre triângulos, quadrados e outros polígonos, portanto faz sentido definir uma nova classe de interface só para eles: IPolygon. A interface IPolygon também é muito simples, conforme mostrado aqui:

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

Essa interface define dois métodos comuns a todos os polígonos: o método getPerimeter() que mede a distância combinada de todos os lados e o método getSumOfAngles() que adiciona todos os ângulos internos.

A interface IPolygon estende a interface IGeometricShape, o que significa que qualquer classe que implemente a interface IPolygon deve declarar os quatro métodos, dois da interface IGeometricShape e dois da interface IPolygon.

Definição das classes Shape

Depois que você tiver uma boa idéia sobre os métodos comuns a cada tipo de forma, você pode definir as próprias classes. Em termos da quantidade dos métodos precisam ser implementados, a forma mais simples é a da classe Circle, mostrada aqui:

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

A classe Circle implementa a interface IGeometricShape, portanto ela deve fornecer código para os métodos getArea() e describe(). Além disso, ela define o método getCircumference() que é exclusivo à classe Circle. A classe Circle também declara uma propriedade, diameter que não é encontrada nas outras classes de polígonos.

Os dois outros tipos de formas, quadrados e triângulos eqüiláteros, têm algumas outras coisas em comum: cada um deles têm lados com o mesmo comprimento e há fórmulas comuns que você pode usar para calcular o perímetro e a soma dos ângulos internos dos dois. Na verdade, essas fórmulas comuns são aplicadas a todos os outros polígonos regulares que você definir no futuro.

A classe RegularPolygon é a superclasse das classes Square e EquilateralTriangle. Uma superclasse permite definir métodos comuns em um lugar, portanto você não precisa defini-los separadamente em cada subclasse. Este é o código da 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; 
        } 
    } 
}

Primeiro, a classe RegularPolygon declara duas propriedades comuns a todos os polígonos regulares: o comprimento de cada lado (a propriedade sideLength) e o número de lados (a propriedade numSides).

A classe RegularPolygon implementa a interface IPolygon e declara os quatro métodos da interface IPolygon. Ela implementa dois desses, os métodos getPerimeter() e getSumOfAngles(), usando fórmulas comuns.

Como a fórmula do método getArea() é diferente de forma para forma, a versão da classe base do método não pode incluir a lógica comum que pode ser herdada pelos métodos da subclasse. Em vez disso, ele simplesmente retorna um valor padrão 0 para indicar que a área não foi calculada. Para calcular a área de cada forma corretamente, as próprias subclasses da classe RegularPolygon precisam substituir o método getArea().

O seguinte código da classe EquilateralTriangle mostra como o método getArea() é substituído:

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

A palavra-chave override indica que o método EquilateralTriangle.getArea() substitui intencionalmente o método getArea() da superclasse RegularPolygon. Quando o método EquilateralTriangle.getArea() é chamado, ele calcula a área usando a fórmula do código anterior, e o código no método RegularPolygon.getArea() nunca é executado.

Em comparação, a classe EquilateralTriangle não define sua própria versão do método getPerimeter(). Quando o método EquilateralTriangle.getPerimeter() é chamado, a chamada percorre a cadeia de herança e executa o código no método getPerimeter() da superclasse RegularPolygon.

O construtor EquilateralTriangle() usa a instrução super() para chamar explicitamente o construtor RegularPolygon() de sua superclasse. Se os dois construtores tivessem o mesmo conjunto de parâmetros, você poderia omitir completamente o construtor EquilateralTriangle() e o construtor RegularPolygon() seria executado. No entanto, o construtor RegularPolygon() precisa de um parâmetro extra, numSides. Portanto, o construtor EquilateralTriangle() chama super(len, 3) que passa o parâmetro de entrada len e o valor 3 para indicar que o triângulo tem três lados.

O método describe() também usa a instrução super(), mas de forma diferente. Ele a utiliza para invocar a versão da superclasse RegularPolygon do método describe(). O método EquilateralTriangle.describe() define primeiro a variável da string desc como uma instrução sobre o tipo da forma. Em seguida, ele obtém os resultados do método RegularPolygon.describe() chamando super.describe() e anexa esse resultado à string desc.

A classe Square não é descrita em detalhes aqui, mas é semelhante à classe EquilateralTriangle, fornecendo um construtor e suas próprias implementações dos métodos getArea() e describe().

Polimorfismo e o método de fábrica

Um conjunto de classes que faz bom uso de interfaces e herança pode ser usado de muitas maneiras interessantes. Por exemplo, todas essas classes de formas descritas até agora implementam a interface IGeometricShape ou estendem uma superclasse que o faz. Portanto, se você definir uma variável como sendo uma ocorrência de IGeometricShape, não precisará saber se ela é realmente uma ocorrência das classes Circle ou Square para chamar seu método describe().

O código a seguir mostra como isso funciona:

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

Quando myShape.describe() é chamado, ele executa o método Circle.describe(), porque embora a variável esteja definida como uma ocorrência da interface IGeometricShape, Circle é sua classe subjacente.

Este exemplo mostra o princípio do polimorfismo em ação: a mesma chamada de método exata resulta na execução de código diferente, dependendo da classe do objeto cujo método está sendo chamado.

O aplicativo GeometricShapes aplica esse tipo de polimorfismo com base em interface usando uma versão simplificada de um padrão de design conhecido como método de fábrica. O termo método de fábrica significa uma função que retorna um objeto cujo tipo de dados subjacentes ou conteúdo pode ser diferente, dependendo do contexto.

A classe GeometricShapeFactory mostrada aqui define um método de fábrica denominado 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(); 
        } 
    } 
}

O método de fábrica createShape() permite que construtores de subclasses de formas definam os detalhes das ocorrências que eles criam, enquanto retornam os novos objetos como ocorrências de IGeometricShape para que eles possam ser manipulados pelo aplicativo de maneira mais geral.

O método describeShape() no exemplo anterior mostra como um aplicativo pode usar o método de fábrica para obter uma referência genérica para um objeto mais específico. O aplicativo pode obter a descrição de um objeto Circle criado recentemente, como este:

GeometricShapeFactory.describeShape("Circle", 100);

Em seguida, o método describeShape() chama o método de fábrica createShape() com os mesmo parâmetros, armazenando o novo objeto Circle em uma variável estática denominada currentShape que foi digitada como um objeto IGeometricShape. Em seguida, o método describe() é chamado no objeto currentShape, e essa chamada é resolvida automaticamente para executar o método Circle.describe() retornando uma descrição detalhada do círculo.

Aprimoramento do aplicativo de amostra

O poder real das interfaces e da herança torna-se aparente quando você aprimora ou altera o aplicativo.

Suponha que você deseja adicionar uma nova forma, um pentágono, a este aplicativo de amostra. Você criaria uma classe Pentagon que estende a classe RegularPolygon e define suas próprias versões dos métodos getArea() e describe(). Em seguida, adicionaria uma nova opção de Pentagon à caixa de combinação na interface de usuário do aplicativo. Mas isso é tudo. A classe Pentagon obteria automaticamente a funcionalidade dos métodos getPerimeter() e getSumOfAngles() da classe RegularPolygon por herança. Como ela é herdada de uma classe que implementa a interface IGeometricShape, uma ocorrência Pentagon também pode ser tratada como uma ocorrência IGeometricShape. Isso significa que para adicionar um novo tipo de forma, não é preciso alterar a assinatura de nenhum dos métodos na classe GeometricShapeFactory (e, conseqüentemente, também não é preciso alterar nenhum código que use a classe GeometricShapeFactory).

Talvez você queira adicionar uma classe Pentagon ao exemplo de Geometric Shapes como um exercício, para ver como as interfaces e a herança podem facilitar a carga de trabalho da adição de novos recursos a um aplicativo.