Temas avanzados

Historia de la implementación de la programación orientada a objetos en ActionScript

Como el diseño de ActionScript 3.0 se ha basado en las versiones anteriores de ActionScript, puede resultar útil comprender la evolución del modelo de objetos de ActionScript. ActionScript empezó siendo un mecanismo sencillo de creación de scripts para las primeras versiones de Flash Professional. Con el tiempo, los programadores empezaron a crear aplicaciones cada vez más complejas con ActionScript. En respuesta a las necesidades de los programadores, en cada nueva versión se añadieron características al lenguaje para facilitar la creación de aplicaciones complejas.

ActionScript 1.0

ActionScript 1.0 es la versión del lenguaje utilizada en Flash Player 6 y en versiones anteriores. En esta fase inicial del desarrollo, el modelo de objetos de ActionScript ya se basaba en el concepto de objeto como tipo de datos básico. Un objeto de ActionScript es un tipo de datos compuesto con un conjunto de propiedades. Al describir el modelo de objetos, el término propiedades abarca todo lo que está asociado a un objeto, como las variables, las funciones o los métodos.

Aunque esta primera generación de ActionScript no permitía definir clases con una palabra clave class, se podía definir una clase mediante un tipo de objeto especial denominado objeto prototipo. En lugar de utilizar una palabra clave class para crear una definición de clase abstracta a partir de la cual se puedan crear instancias de objetos, como se hace en otros lenguajes basados en clases como Java y C++, los lenguajes basados en prototipos como ActionScript 1.0 utilizan un objeto existente como modelo (o prototipo) para crear otros objetos. En un lenguaje basado en clases, los objetos pueden señalar a una clase como su plantilla; en cambio, en un lenguaje basado en prototipos los objetos señalan a otro objeto, el prototipo, que es su plantilla.

Para crear una clase en ActionScript 1.0, se define una función constructora para dicha clase. En ActionScript, las funciones son objetos reales, no sólo definiciones abstractas. La función constructora que se crea sirve como objeto prototipo para las instancias de dicha clase. El código siguiente crea una clase denominada Shape y define una propiedad denominada visible establecida en true de manera predeterminada:

// base class 
function Shape() {} 
// Create a property named visible. 
Shape.prototype.visible = true;

Esta función constructora define una clase Shape de la que se puede crear una instancia mediante el operador new, de la manera siguiente:

myShape = new Shape();

De la misma manera que el objeto de función constructora de Shape() es el prototipo para instancias de la clase Shape, también puede ser el prototipo para subclases de Shape (es decir, otras clases que amplíen la clase Shape).

La creación de una clase que es una subclase de la clase Shape es un proceso en dos pasos. En primer lugar debe crearse la clase definiendo una función constructora, de la manera siguiente:

// child class 
function Circle(id, radius) 
{ 
this.id = id; 
this.radius = radius; 
}

En segundo lugar, se utiliza el operador new para declarar que la clase Shape es el prototipo para la clase Circle. De manera predeterminada, cualquier clase que se cree utilizará la clase Object como prototipo, lo que significa que Circle.prototype contiene actualmente un objeto genérico (una instancia de la clase Object). Para especificar que el prototipo de Circle es Shape y no Object, se debe utilizar el código siguiente para cambiar el valor de Circle.prototype de forma que contenga un objeto Shape en lugar de un objeto genérico:

// Make Circle a subclass of Shape. 
Circle.prototype = new Shape();

La clase Shape y la clase Circle ahora están vinculadas en una relación de herencia que se suele llamar cadena de prototipos. El diagrama ilustra las relaciones de una cadena de prototipos:

La clase base al final de cada cadena de prototipos es la clase Object. La clase Object contiene una propiedad estática denominada Object.prototype que señala al objeto prototipo base para todos los objetos creados en ActionScript 1.0. El siguiente objeto de la cadena de prototipos de ejemplo es el objeto Shape. Esto se debe a que la propiedad Shape.prototype no se ha establecido explícitamente, por lo que sigue conteniendo un objeto genérico (una instancia de la clase Object). El vínculo final de esta cadena es la clase Circle, que está vinculada a su prototipo, la clase Shape (la propiedad Circle.prototype contiene un objeto Shape).

Si se crea una instancia de la clase Circle, como en el siguiente ejemplo, la instancia hereda la cadena de prototipos de la clase Circle:

// Create an instance of the Circle class. 
myCircle = new Circle();

Antes se creó una propiedad denominada visible como un miembro de la clase Shape. En este ejemplo, la propiedad visible no existe como parte del objeto myCircle, sólo existe como miembro del objeto Shape; sin embargo, la siguiente línea de código devuelve true:

trace(myCircle.visible); // output: true

El motor de ejecución puede determinar que el objeto myCircle hereda la propiedad visible recorriendo la cadena de prototipos. Al ejecutar este código, el motor de ejecución busca primero una propiedad denominada visible en las propiedades del objeto myCircle, pero no la encuentra. A continuación, busca en el objeto Circle.prototype, pero sigue sin encontrar una propiedad denominada visible. Sigue por la cadena de prototipos hasta que encuentra la propiedad visible definida en el objeto Shape.prototype y devuelve el valor de dicha propiedad.

Para simplificar se omiten muchos de los detalles y las complejidades de la cadena de prototipos. El objetivo es proporcionar información suficiente para comprender el modelo de objetos de ActionScript 3.0.

ActionScript 2.0

En ActionScript 2.0 se incluyeron palabras clave nuevas, como class, extends, public y private, que permitían definir clases de una manera familiar para cualquiera que trabaje con lenguajes basados en clases como Java y C++. Es importante saber que el mecanismo de herencia subyacente no ha cambiado entre ActionScript 1.0 y ActionScript 2.0. En ActionScript 2.0 simplemente se ha añadido una nueva sintaxis para la definición de clases. La cadena de prototipos funciona de la misma manera en ambas versiones del lenguaje.

La nueva sintaxis introducida en ActionScript 2.0, que se muestra en el siguiente fragmento, permite definir clases de una manera más intuitiva para muchos programadores:

// base class 
class Shape 
{ 
var visible:Boolean = true; 
}

En ActionScript 2.0 también se introdujeron las anotaciones de tipos de datos para la verificación de tipos en tiempo de compilación. Esto permite declarar que la propiedad visible del ejemplo anterior sólo debe contener un valor booleano. La nueva palabra clave extends también simplifica el proceso de crear una subclase. En el siguiente ejemplo, el proceso que requería dos pasos en ActionScript 1.0 se realiza con un solo paso mediante la palabra clave extends:

// child class 
class Circle extends Shape 
{ 
    var id:Number; 
    var radius:Number; 
    function Circle(id, radius) 
    { 
        this.id = id; 
        this.radius = radius; 
        } 
}

Ahora el constructor se declara como parte de la definición de clase y las propiedades de clase id y radius también deben declararse explícitamente.

En ActionScript 2.0 también se permite definir interfaces, lo que permite refinar los programas orientados a objetos con protocolos definidos formalmente para la comunicación entre objetos.

El objeto de clase de ActionScript 3.0

Un paradigma común de la programación orientada a objetos, que se suele asociar con Java y C++, utiliza las clases para definir tipos de objetos. Los lenguajes de programación que adoptan este paradigma también tienden a utilizar clases para crear instancias del tipo de datos definido por la clase. ActionScript utiliza clases para ambos propósitos, pero sus raíces como lenguaje basado en prototipos le añaden una característica interesante. ActionScript crea para cada definición de clase un objeto de clase especial que permite compartir el comportamiento y el estado. Sin embargo, para muchos programadores que utilizan ActionScript, esta distinción puede no tener ninguna consecuencia práctica en la programación. ActionScript 3.0 se ha diseñado de forma que se puedan crear sofisticadas aplicaciones orientadas a objetos sin tener que utilizar, ni si quiera comprender, estos objetos de clase especiales.

En el diagrama siguiente se muestra la estructura de un objeto de clase que representa una clase simple denominada A que se define con la sentencia class A {}:

Cada rectángulo del diagrama representa un objeto. Cada objeto del diagrama tiene un carácter de subíndice para representar que pertenece a la clase A. El objeto de clase (CA) contiene referencias a una serie de otros objetos importantes. Un objeto traits de instancia (TA) almacena las propiedades de instancia definidas en una definición de clase. Un objeto traits de clase (TCA) representa el tipo interno de la clase y almacena las propiedades estáticas definidas por la clase (el carácter de subíndice C significa “class”). El objeto prototipo (PA) siempre hace referencia al objeto de clase al que se asoció originalmente mediante la propiedad constructor.

El objeto traits

El objeto traits, que es una novedad en ActionScript 3.0, se implementó para mejorar el rendimiento. En versiones anteriores de ActionScript, la búsqueda de nombres era un proceso lento, ya que Flash Player recorría la cadena de prototipos. En ActionScript 3.0, la búsqueda de nombres es mucho más rápida y eficaz, ya que las propiedades heredadas se copian desde las superclases al objeto traits de las subclases.

No se puede acceder directamente al objeto traits desde el código, pero las mejoras de rendimiento y de uso de la memoria reflejan el efecto que produce. El objeto traits proporciona a la AVM2 información detallada sobre el diseño y el contenido de una clase. Con este conocimiento, la AVM2 puede reducir considerablemente el tiempo de ejecución, ya que generalmente podrá generar instrucciones directas de código máquina para acceder a las propiedades o llamar a métodos directamente sin tener que realizar una búsqueda lenta de nombres.

Gracias al objeto traits, la huella en la memoria de un objeto puede ser considerablemente menor que la de un objeto similar en las versiones anteriores de ActionScript. Por ejemplo, si una clase está cerrada (es decir, no se declara como dinámica), una instancia de la clase no necesita una tabla hash para propiedades añadidas dinámicamente y puede contener poco más que un puntero a los objetos traits y espacio para las propiedades fijas definidas en la clase. En consecuencia, un objeto que requería 100 bytes de memoria en ActionScript 2.0 puede requerir tan sólo 20 bytes de memoria en ActionScript 3.0.

Nota: el objeto traits es un detalle de implementación interna y no hay garantías de que no cambie o incluso desaparezca en versiones futuras de ActionScript.

El objeto prototype

Cada clase de objeto de ActionScript tiene una propiedad denominada prototype, que es una referencia al objeto prototipo de la clase. El objeto prototipo es un legado de las raíces de ActionScript como lenguaje basado en prototipos. Para obtener más información, consulte Historia de la implementación de la programación orientada a objetos en ActionScript.

La propiedad prototype es de sólo lectura, lo que significa que no se puede modificar para que señale a otros objetos. Esto ha cambiado con respecto a la propiedad prototype de una clase en las versiones anteriores de ActionScript, en las que se podía reasignar el prototipo de forma que señalara a una clase distinta. Aunque la propiedad prototype es de sólo lectura, el objeto prototipo al que hace referencia no lo es. Es decir, se pueden añadir propiedades nuevas al objeto prototipo. Las propiedades añadidas al objeto prototipo son compartidas por todas las instancias de la clase.

La cadena de prototipos, que fue el único mecanismo de herencia en versiones anteriores de ActionScript, sirve únicamente como función secundaria en ActionScript 3.0. El mecanismo de herencia principal, herencia de propiedades fijas, se administra internamente mediante el objeto traits. Una propiedad fija es una variable o un método que se define como parte de una definición de clase. La herencia de propiedades fijas también se denomina herencia de clase porque es el mecanismo de herencia asociado con palabras clave como class, extends y override.

La cadena de prototipos proporciona un mecanismo de herencia alternativo que es más dinámico que la herencia de propiedades fijas. Se pueden añadir propiedades a un objeto prototipo de la clase no sólo como parte de la definición de clase, sino también en tiempo de ejecución mediante la propiedad prototype del objeto de clase. Sin embargo, hay que tener en cuenta que si se establece el compilador en modo estricto, es posible que no se pueda acceder a propiedades añadidas a un objeto prototipo a menos que se declare una clase con la palabra clave dynamic.

La clase Object es un buen ejemplo de clase con varias propiedades asociadas al objeto prototipo. Los métodos toString() y valueOf() de la clase Object son en realidad funciones asignadas a propiedades del objeto prototipo de la clase Object. A continuación se muestra un ejemplo del aspecto que tendría en teoría la declaración de estos métodos (la implementación real difiere ligeramente a causa de los detalles de implementación):

public dynamic class Object 
{ 
    prototype.toString = function() 
    { 
        // statements 
    }; 
    prototype.valueOf = function() 
    { 
        // statements 
    }; 
}

Como se mencionó anteriormente, se puede asociar una propiedad a un objeto prototipo de clase fuera de la definición de clase. Por ejemplo, el método toString() también se puede definir fuera de la definición de clase Object, de la manera siguiente:

Object.prototype.toString = function() 
{ 
    // statements 
};

Sin embargo, a diferencia de la herencia de propiedades fijas, la herencia de prototipo no requiere la palabra clave override si se desea volver a definir un método en una subclase. Por ejemplo, si se desea volver a definir el método valueOf() en una subclase de la clase Object, hay tres opciones. En primer lugar, se puede definir un método valueOf() en el objeto prototipo de la subclase, dentro de la definición de la clase. El código siguiente crea una subclase de Object denominada Foo y redefine el método valueOf() en el objeto prototipo de Foo como parte de la definición de clase. Como todas las clases heredan de Object, no es necesario utilizar la palabra clave extends.

dynamic class Foo 
{ 
    prototype.valueOf = function() 
    { 
        return "Instance of Foo"; 
    }; 
}

En segundo lugar, se puede definir un método valueOf() en el objeto prototipo de Foo, fuera de la definición de clase, como se indica en el código siguiente:

Foo.prototype.valueOf = function() 
{ 
    return "Instance of Foo"; 
};

Por último, se puede definir una propiedad fija denominada valueOf() como parte de la clase Foo. Esta técnica difiere de las otras en que mezcla la herencia de propiedades fijas con la herencia de prototipo. Cualquier subclase de Foo que vaya a redefinir valueOf() debe utilizar la palabra clave override. El código siguiente muestra valueOf() definido como una propiedad fija en Foo:

class Foo 
{ 
    function valueOf():String 
    { 
        return "Instance of Foo"; 
    } 
}

El espacio de nombres AS3

La existencia de dos mecanismos de herencia independientes, la herencia de propiedades fijas y la herencia de prototipo, crea un reto interesante para la compatibilidad con respecto a las propiedades y los métodos de las clases principales. La compatibilidad con la especificación del lenguaje ECMAScript en la que se basa ActionScript, requiere utilizar herencia de prototipo, lo que significa que las propiedades y los métodos de una clase principal se definen en el objeto prototipo de esa clase. Por otra parte, la compatibilidad con ActionScript 3.0 requiere utilizar la herencia de propiedades fijas, lo que significa que las propiedades y los métodos de una clase principal se definen en la definición de clase mediante las palabras clave const, var y function. Además, el uso de propiedades fijas en lugar de las versiones de prototipos puede proporcionar un aumento considerable de rendimiento en tiempo de ejecución.

ActionScript 3.0 resuelve este problema utilizando tanto la herencia de prototipo como la herencia de propiedades fijas para las clases principales. Cada clase principal contiene dos conjuntos de propiedades y métodos. Un conjunto se define en el objeto prototipo por compatibilidad con la especificación ECMAScript y el otro conjunto se define con propiedades fijas y el espacio de nombres AS3.0 por compatibilidad con ActionScript 3.0.

El espacio de nombres AS3 proporciona un cómodo mecanismo para elegir entre los dos conjuntos de propiedades y métodos. Si no se utiliza el espacio de nombres AS3, una instancia de una clase principal hereda las propiedades y métodos definidos en el objeto prototipo de la clase principal. Si se decide utilizar el espacio de nombres AS3, una instancia de una clase principal hereda las versiones de AS3, ya que siempre se prefieren las propiedades fijas a las propiedades de prototipo. Es decir, siempre que una propiedad fija está disponible, se utilizará en lugar de una propiedad de prototipo con el mismo nombre.

Se puede utilizar de forma selectiva la versión del espacio de nombres AS3 de una propiedad o un método calificándolo con el espacio de nombres AS3. Por ejemplo, el código siguiente utiliza la versión AS3 del método Array.pop():

var nums:Array = new Array(1, 2, 3); 
nums.AS3::pop(); 
trace(nums); // output: 1,2

Como alternativa, se puede utilizar la directiva use namespace para abrir el espacio de nombres AS3 para todas las definiciones de un bloque de código. Por ejemplo, el código siguiente utiliza la directiva use namespace para abrir el espacio de nombres AS3 para los métodos pop() y push():

use namespace AS3; 
 
var nums:Array = new Array(1, 2, 3); 
nums.pop(); 
nums.push(5); 
trace(nums) // output: 1,2,5

ActionScript 3.0 también proporciona opciones de compilador para cada conjunto de propiedades de forma que se pueda aplicar el espacio de nombres AS3 a todo el programa. La opción de compilador -as3 representa el espacio de nombres AS3 y la opción de compilador -es representa la opción de herencia de prototipo (es significa ECMAScript). Para abrir el espacio de nombres AS3 para todo el programa, se debe establecer la opción de compilador -as3 en true y la opción de compilador -es en false. Para utilizar las versiones de prototipos, las opciones de compilador deben establecerse en los valores opuestos. La configuración de compilador predeterminada para Flash Builder y Flash Professional es -as3 = true y -es = false.

Si se pretende ampliar alguna de las clases principales y sustituir algún método, hay que comprender cómo puede afectar el espacio de nombres AS3 a la manera de declarar un método sustituido. Si se utiliza el espacio de nombres AS3, cualquier método sustituto de un método de clase principal también debe utilizar el espacio de nombres AS3, junto con el atributo override. Si no se utiliza el espacio de nombres AS3 y se desea redefinir un método de clase principal en una subclase, no se debe utilizar el espacio de nombres AS3 ni la palabra clave override.