Ejemplo: GeometricShapes

La aplicación de ejemplo GeometricShapes muestra cómo se pueden aplicar algunos conceptos y características de la orientación a objetos con ActionScript 3.0:

  • Definición de clases

  • Ampliación de clases

  • Polimorfismo y la palabra clave override

  • Definición, ampliación e implementación de interfaces

También incluye un “método de fábrica” que crea instancias de clase y muestra la manera de declarar un valor devuelto como una instancia de una interfaz y utilizar ese objeto devuelto de forma genérica.

Para obtener los archivos de la aplicación de este ejemplo, consulte www.adobe.com/go/learn_programmingAS3samples_flash_es. Los archivos de la aplicación GeometricShapes se encuentran en la carpeta Samples/GeometricShapes. La aplicación consta de los siguientes archivos:

Archivo

Descripción

GeometricShapes.mxml

o

GeometricShapes.fla

El archivo de aplicación principal en Flash (FLA) o Flex (MXML)

com/example/programmingas3/geometricshapes/IGeometricShape.as

Los métodos básicos de definición de interfaz que se van a implementar en todas las clases de la aplicación GeometricShapes.

com/example/programmingas3/geometricshapes/IPolygon.as

Una interfaz que define los métodos que se van a implementar en las clases de la aplicación GeometricShapes que tienen varios lados.

com/example/programmingas3/geometricshapes/RegularPolygon.as

Un tipo de forma geométrica que tiene lados de igual longitud, simétricamente ubicados alrededor del centro de la forma.

com/example/programmingas3/geometricshapes/Circle.as

Un tipo de forma geométrica que define un círculo.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Una subclase de RegularPolygon que define un triángulo con todos los lados de la misma longitud.

com/example/programmingas3/geometricshapes/Square.as

Una subclase de RegularPolygon que define un rectángulo con los cuatro lados de la misma longitud.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Una clase que contiene un método de fábrica para crear formas de un tipo y un tamaño específicos.

Definición de las clases de GeometricShapes

La aplicación GeometricShapes permite al usuario especificar un tipo de forma geométrica y un tamaño. Responde con una descripción de la forma, su área y su perímetro.

La interfaz de usuario de la aplicación es trivial: incluye algunos controles para seleccionar el tipo de forma, establecer el tamaño y mostrar la descripción. La parte más interesante de esta aplicación está bajo la superficie, en la estructura de las clases y las interfaces.

Esta aplicación manipula formas geométricas, pero no las muestra gráficamente.

Las clases e interfaces que definen las formas geométricas en este ejemplo se muestran en el diagrama siguiente con notación UML (Unified Modeling Language):

Clases de ejemplo GeometricShapes

Definición del comportamiento común en una interfaz

La aplicación GeometricShapes trata con tres tipos de formas: círculos, cuadrados y triángulos equiláteros. La estructura de la clase GeometricShapes empieza por una interfaz muy sencilla, IGeometricShape, que muestra métodos comunes a los tres tipos de formas:

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

La interfaz define dos métodos: getArea(), que calcula y devuelve el área de la forma y describe(), que crea una descripción textual de las propiedades de la forma.

También se pretende obtener el perímetro de cada forma. Sin embargo, el perímetro de un círculo es su circunferencia y se calcula de una forma única, por lo que en el caso del círculo el comportamiento es distinto del de un triángulo o un cuadrado. De todos modos, los triángulos, los cuadrados y otros polígonos son suficientemente similares, así que tiene sentido definir una nueva clase de interfaz para ellos: IPolygon. La interfaz IPolygon también es bastante sencilla, como se muestra a continuación:

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

Esta interfaz define dos métodos comunes para todos los polígonos: el método getPerimeter() que mide la distancia combinada de todos los lados y el método getSumOfAngles() que suma todos los ángulos interiores.

La interfaz IPolygon amplía la interfaz IGeometricShape, lo que significa que cualquier clase que implemente la interfaz IPolygon debe declarar los cuatro métodos: dos de la interfaz IGeometricShape y dos de la interfaz IPolygon.

Definición de las clases de formas

Cuando ya se conozcan los métodos comunes a cada tipo de forma, se pueden definir las clases de las formas. Por la cantidad de métodos que hay que implementar, la forma más sencilla es la clase Circle, que se muestra a continuación:

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 clase Circle implementa la interfaz IGeometricShape, por lo que hay que proporcionar código para el método getArea() y el método describe(). Además, define el método getCircumference(), que es único para la clase Circle. La clase Circle también declara una propiedad, diameter, que no se encuentra en las clases de los otros polígonos.

Los otros dos tipos de formas, cuadrados y triángulos equiláteros, tienen algunos aspectos en común: cada uno de ellos tiene lados de longitud simular y existen fórmulas comunes que se pueden utilizar para calcular el perímetro y la suma de los ángulos interiores para ambos. De hecho, esas fórmulas comunes se aplicarán a cualquier otro polígono regular que haya que definir en el futuro.

La clase RegularPolygon será la superclase para la clase Square y la clase EquilateralTriangle. Una superclase permite centralizar la definición de métodos comunes de forma que no sea necesario definirlos por separado en cada subclase. A continuación se muestra el código para la clase 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; 
        } 
    } 
}

En primer lugar, la clase RegularPolygon declara dos propiedades que son comunes a todos los polígonos regulares: la longitud de cada lado (propiedad sideLength) y el número de lados (propiedad numSides).

La clase RegularPolygon implementa la interfaz IPolygon y declara los cuatro métodos de la interfaz IPolygon. Implementa dos de ellos, getPerimeter() y getSumOfAngles(), utilizando fórmulas comunes.

Como la fórmula para el método getArea() varía de una forma a otra, la versión de la clase base del método no puede incluir lógica común que pueda ser heredada por los métodos de la subclase. Sólo devuelve el valor predeterminado 0, para indicar que no se ha calculado el área. Para calcular el área de cada forma correctamente, las subclases de la clase RegularPolygon tendrán que sustituir el método getArea().

El código siguiente para la clase EquilateralTriangle muestra cómo se sustituye el método 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 palabra clave override indica que el método EquilateralTriangle.getArea() sustituye de forma intencionada el método getArea() de la superclase RegularPolygon. Cuando se llama al método EquilateralTriangle.getArea(), se calcula el área con la fórmula del fragmento de código anterior y no se ejecuta el código del método RegularPolygon.getArea().

En cambio, la clase EquilateralTriangle no define su propia versión del método getPerimeter(). Cuando se llama al método EquilateralTriangle.getPerimeter(), la llamada sube por la cadena de herencia y ejecuta el código del método getPerimeter() de la superclase RegularPolygon.

El constructor de EquilateralTriangle() utiliza la sentencia super() para invocar explícitamente el constructor de RegularPolygon() de su superclase. Si ambos constructores tuvieran el mismo conjunto de parámetros, se podría omitir el constructor de EquilateralTriangle() y se ejecutaría en su lugar el constructor de RegularPolygon(). No obstante, el constructor de RegularPolygon() requiere un parámetro adicional, numSides. Así, el constructor de EquilateralTriangle() llama a super(len, ), que pasa el parámetro de entrada len y el valor 3 para indicar que el triángulo tendrá tres lados.

El método describe() también utiliza la sentencia super(), pero de un modo diferente. La utiliza para invocar a la versión de la superclase RegularPolygon del métododescribe(). El método EquilateralTriangle.describe() establece primero la variable de cadena desc en una declaración del tipo de forma. A continuación, obtiene los resultados del método RegularPolygon.describe() llamando a super.describe() y añade el resultado a la cadena desc.

No se proporciona en esta sección una descripción detallada de la clase Square, pero es similar a la clase EquilateralTriangle; proporciona un constructor y su propia implementación de los métodos getArea() y describe().

Polimorfismo y el método de fábrica

Un conjunto de clases que haga buen uso de las interfaces y la herencia se puede utilizar de muchas maneras interesantes. Por ejemplo, todas las clases de formas descritas hasta ahora implementan la interfaz IGeometricShape o amplían una superclase que lo hace. Así, si se define una variable como una instancia de IGeometricShape, no hay que saber si es realmente una instancia de la clase Circle o de la clase Square para llamar a su método describe().

El código siguiente muestra cómo funciona esto:

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

Cuando se llama a myShape.describe(), se ejecuta el método Circle.describe() ya que, aunque la variable se define como una instancia de la interfaz IGeometricShape, Circle es su clase subyacente.

En este ejemplo se muestra el principio del polimorfismo en activo: la misma llamada al método tiene como resultado la ejecución de código diferente, dependiendo de la clase del objeto cuyo método se esté invocando.

La aplicación GeometricShapes aplica este tipo de polimorfismo basado en interfaz con una versión simplificada de un patrón de diseño denominado método de fábrica. El término método de fábrica hace referencia a una función que devuelve un objeto cuyo tipo de datos o contenido subyacente puede variar en función del contexto.

La clase GeometricShapeFactory mostrada define un 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(); 
        } 
    } 
}

El método de fábrica createShape() permite a los constructores de subclases de formas definir los detalles de las instancias que crean, devolviendo los objetos nuevos como instancias de IGeometricShape de forma que puedan ser manipulados por la aplicación de una manera más general.

El método describeShape() del ejemplo anterior muestra cómo se puede utilizar el método de fábrica en una aplicación para obtener una referencia genérica a un objeto más específico. La aplicación puede obtener la descripción para un objeto Circle recién creado así:

GeometricShapeFactory.describeShape("Circle", 100);

A continuación, el método describeShape() llama al método de fábrica createShape() con los mismos parámetros y almacena el nuevo objeto Circle en una variable estática denominada currentShape, a la que se le asignó el tipo de un objeto IGeometricShape. A continuación, se llama al método describe() en el objeto currentShape y se resuelve esa llamada automáticamente para ejecutar el método Circle.describe(), lo que devuelve una descripción detallada del círculo.

Mejora de la aplicación de ejemplo

La verdadera eficacia de las interfaces y la herencia se aprecia al ampliar o cambiar la aplicación.

Por ejemplo, se puede añadir una nueva forma (un pentágono) a esta aplicación de ejemplo. Para ello se crea una clase Pentagon que amplía la clase RegularPolygon y define sus propias versiones de los métodos getArea() y describe(). A continuación, se añade una nueva opción Pentagon al cuadro combinado de la interfaz de usuario de la aplicación. Y ya está. La clase Pentagon recibirá automáticamente la funcionalidad de los métodos getPerimeter() y getSumOfAngles() de la clase RegularPolygon por herencia. Como hereda de una clase que implementa la interfaz IGeometricShape, una instancia de Pentagon puede tratarse también como una instancia de IGeometricShape. Esto significa que para agregar un nuevo tipo de forma, no es necesario cambiar la firma del método de ningún método en la clase GeometricShapeFactory (y por lo tanto, tampoco no es necesario modificar ninguna parte de código que utilice la clase GeometricShapeFactory).

Se puede añadir una clase Pentagon al ejemplo Geometric Shapes como ejercicio, para ver cómo facilitan las interfaces y la herencia el trabajo de añadir nuevas características a una aplicación.