Herencia

La herencia es una forma de reutilización de código que permite a los programadores desarrollar clases nuevas basadas en clases existentes. Las clases existentes se suelen denominar clases base o superclases, y las clases nuevas se denominan subclases. Una ventaja clave de la herencia es que permite reutilizar código de una clase base manteniendo intacto el código existente. Además, la herencia no requiere realizar ningún cambio en la interacción entre otras clases y la clase base. En lugar de modificar una clase existente que puede haber sido probada minuciosamente o que ya se está utilizando, la herencia permite tratar esa clase como un módulo integrado que se puede ampliar con propiedades o métodos adicionales. Se utiliza la palabra clave extends para indicar que una clase hereda de otra clase.

La herencia también permite beneficiarse del polimorfismo en el código. El polimorfismo es la capacidad de utilizar un solo nombre de método para un método que se comporta de distinta manera cuando se aplica a distintos tipos de datos. Un ejemplo sencillo es una clase base denominada Shape con dos subclases denominadas Circle y Square. La clase Shape define un método denominado area() que devuelve el área de la forma. Si se implementa el polimorfismo, se puede llamar al método area() en objetos de tipo Circle y Square, y se harán automáticamente los cálculos correctos. La herencia activa el polimorfismo al permitir que las subclases hereden y redefinan (sustituyan) los métodos de la clase base. En el siguiente ejemplo, se redefine el método area() mediante las clases Circle y Square:

class Shape 
{ 
    public function area():Number 
    { 
        return NaN; 
    } 
} 
 
class Circle extends Shape 
{ 
    private var radius:Number = 1; 
    override public function area():Number 
    { 
        return (Math.PI * (radius * radius)); 
    } 
} 
 
class Square extends Shape 
{ 
    private var side:Number = 1; 
    override public function area():Number 
    { 
        return (side * side); 
    } 
} 
 
var cir:Circle = new Circle(); 
trace(cir.area()); // output: 3.141592653589793 
var sq:Square = new Square(); 
trace(sq.area()); // output: 1

Como cada clase define un tipo de datos, el uso de la herencia crea una relación especial entre una clase base y una clase que la amplía. Una subclase posee todas las propiedades de su clase base, lo que significa que siempre se puede sustituir una instancia de una subclase como una instancia de la clase base. Por ejemplo, si un método define un parámetro de tipo Shape, se puede pasar un argumento de tipo Circle, ya que Circle amplía Shape, como se indica a continuación:

function draw(shapeToDraw:Shape) {} 
 
var myCircle:Circle = new Circle(); 
draw(myCircle);

Herencia y propiedades de instancia

Todas las subclases heredan una propiedad de instancia definida con la palabra clave function, var o const, con tal de que no se haya declarado la propiedad con el atributo private en la clase base. Por ejemplo, la clase Event en ActionScript 3.0 tiene diversas subclases que heredan propiedades comunes a todos los objetos de evento.

Para algunos tipos de eventos, la clase Event contiene todas las propiedades necesarias para definir el evento. Estos tipos de eventos no requieren más propiedades de instancia que las definidas en la clase Event. Algunos ejemplos de estos eventos son el evento complete, que se produce cuando los datos se han cargado correctamente, y el evento connect, que se produce cuando se establece una conexión de red.

El ejemplo siguiente es un fragmento de la clase Event que muestra algunas de las propiedades y los métodos que heredarán las subclases. Como las propiedades se heredan, una instancia de cualquier subclase puede acceder a estas propiedades.

public class Event 
{ 
    public function get type():String; 
    public function get bubbles():Boolean; 
    ... 
 
    public function stopPropagation():void {} 
    public function stopImmediatePropagation():void {} 
    public function preventDefault():void {} 
    public function isDefaultPrevented():Boolean {} 
    ... 
}

Otros tipos de eventos requieren propiedades únicas que no están disponibles en la clase Event. Estos eventos se definen creando subclases de la clase Event para añadir nuevas propiedades a las propiedades definidas en la clase Event. Un ejemplo de una subclase de este tipo es la clase MouseEvent, que añade propiedades únicas a eventos asociados con el movimiento o los clics del ratón, como los eventos mouseMove y click. El ejemplo siguiente es un fragmento de la clase MouseEvent que muestra la definición de propiedades que se han añadido a la subclase y, por tanto, no existen en la clase base:

public class MouseEvent extends Event 
{ 
    public static const CLICK:String= "click"; 
    public static const MOUSE_MOVE:String = "mouseMove"; 
    ... 
 
    public function get stageX():Number {} 
    public function get stageY():Number {} 
    ... 
}

Herencia y especificadores de control de acceso

Si una propiedad se declara con la palabra clave public, estará visible en cualquier parte del código. Esto significa que la palabra clave public, a diferencia de las palabras clave private, protected e internal, no restringe de ningún modo la herencia de propiedades.

Si se declara una propiedad con la palabra clave private, sólo será visible en la clase que la define, lo que significa que ninguna subclase la heredará. Este comportamiento es distinto del de las versiones anteriores de ActionScript, en las que el comportamiento de la palabra clave private era más parecido al de la palabra clave protected de ActionScript 3.0.

La palabra clave protected indica que una propiedad es visible en la clase que la define y en todas sus subclases. A diferencia de la palabra clave protected del lenguaje de programación Java, la palabra clave protected de ActionScript 3.0 no hace que una propiedad esté visible para todas las clases de un mismo paquete. En ActionScript 3.0 sólo las subclases pueden acceder a una propiedad declarada con la palabra clave protected. Además, una propiedad protegida estará visible para una subclase tanto si la subclase está en el mismo paquete que la clase base como si está en un paquete distinto.

Para limitar la visibilidad de una propiedad al paquete en el que se define, se debe utilizar la palabra clave internal o no utilizar ningún especificador de control de acceso. Cuando no se especifica ninguno especificador de control de acceso, el predeterminado es internal. Una propiedad marcada como internal sólo podrá ser heredada por una subclase que resida en el mismo paquete.

Se puede utilizar el ejemplo siguiente para ver cómo afecta cada uno de los especificadores de control de acceso a la herencia más allá de los límites del paquete. El código siguiente define una clase principal de aplicación denominada AccessControl y otras dos clases denominadas Base y Extender. La clase Base está en un paquete denominado foo y la clase Extender, que es una subclase de la clase Base, está en un paquete denominado bar. La clase AccessControl sólo importa la clase Extender y crea una instancia de Extender que intenta acceder a una variable denominada str definida en la clase Base. La variable str se declara como public de forma que el código se compile y ejecute como se indica en el siguiente fragmento:

// Base.as in a folder named foo 
package foo 
{ 
    public class Base 
    { 
        public var str:String = "hello"; // change public on this line 
    } 
} 
 
// Extender.as in a folder named bar 
package bar 
{ 
    import foo.Base; 
    public class Extender extends Base 
    { 
        public function getString():String { 
            return str; 
        } 
    } 
} 
 
// main application class in file named AccessControl.as 
package 
{ 
    import flash.display.MovieClip; 
    import bar.Extender; 
    public class AccessControl extends MovieClip 
    { 
        public function AccessControl() 
        { 
            var myExt:Extender = new Extender(); 
            trace(myExt.str);// error if str is not public 
            trace(myExt.getString()); // error if str is private or internal 
        } 
    } 
}

Para ver cómo afectan los otros especificadores de control de acceso a la compilación y la ejecución del ejemplo anterior, se debe cambiar el especificador de control de acceso de la variable str a private, protected o internal después de eliminar o marcar como comentario la línea siguiente de la clase AccessControl:

trace(myExt.str);// error if str is not public

No se permite la sustitución de variables

Las propiedades declaradas con la palabra clave var o const se pueden heredar, pero no se pueden sustituir. Para sustituir una propiedad hay que redefinirla en una subclase. El único tipo de propiedad que se puede sustituir son los descriptores de acceso get y set (es decir, propiedades declaradas con la palabra clave function). Aunque no es posible sustituir una variable de instancia, se puede obtener una funcionalidad similar creando métodos captador y definidor para la variable de instancia y sustituyendo los métodos.

Sustitución de métodos

Para sustituir un método hay que redefinir el comportamiento de un método heredado. Los métodos estáticos no se heredan y no se pueden sustituir. Sin embargo, las subclases heredan los métodos de instancia, que se pueden sustituir con tal de que se cumplan los dos criterios siguientes:

  • El método de instancia no se ha declarado con la palabra clave final en la clase base. Si se utiliza la palabra clave final con un método de instancia, indica la intención del programador de evitar que las subclases sustituyan el método.

  • El método de instancia no se declara con el especificador de control de acceso private de la clase base. Si se marca un método como private en la clase base, no hay que usar la palabra clave override al definir un método con el mismo nombre en la subclase porque el método de la clase base no será visible para la subclase.

    Para sustituir un método de instancia que cumpla estos criterios, la definición del método en la subclase debe utilizar la palabra clave override y debe coincidir con la versión de la superclase del método en los siguientes aspectos:

  • El método sustituto debe tener el mismo nivel de control de acceso que el método de la clase base. Los métodos marcados como internal tienen el mismo nivel de control de acceso que los métodos que no tienen especificador de control de acceso.

  • El método sustituto debe tener el mismo número de parámetros que el método de la clase base.

  • Los parámetros del método sustituto deben tener las mismas anotaciones de tipo de datos que los parámetros del método de la clase base.

  • El método sustituto debe devolver el mismo tipo de datos que el método de la clase base.

Sin embargo, los nombres de los parámetros del método sustituto no tienen que coincidir con los nombres de los parámetros de la clase base, con tal de que el número de parámetros y el tipo de datos de cada parámetro coincidan.

La sentencia super

Al sustituir un método, los programadores generalmente desean añadir funcionalidad al comportamiento del método de la superclase que van a sustituir, en lugar de sustituir completamente el comportamiento. Esto requiere un mecanismo que permita a un método de una subclase llamar a la versión de sí mismo de la superclase. La sentencia super proporciona este mecanismo, ya que contiene una referencia a la superclase inmediata. El ejemplo siguiente define una clase denominada Base que contiene un método denominado thanks() y una subclase de la clase Base denominada Extender que reemplaza el método thanks(). El método Extender.thanks() utiliza la sentencia super para llamar a Base.thanks().

package { 
    import flash.display.MovieClip; 
    public class SuperExample extends MovieClip 
    { 
        public function SuperExample() 
        { 
            var myExt:Extender = new Extender() 
            trace(myExt.thanks()); // output: Mahalo nui loa 
        } 
    } 
} 
 
class Base { 
    public function thanks():String 
    { 
        return "Mahalo"; 
    } 
} 
 
class Extender extends Base 
{ 
    override public function thanks():String 
    { 
        return super.thanks() + " nui loa"; 
    } 
}

Sustitución de captadores y definidores

Aunque las variables definidas en una superclase no se pueden sustituir, sí se pueden sustituir los captadores y definidores. Por ejemplo, el código siguiente sustituye un captador denominado currentLabel definido en la clase MovieClip en ActionScript 3.0:

package 
{ 
    import flash.display.MovieClip; 
    public class OverrideExample extends MovieClip 
    { 
        public function OverrideExample() 
        { 
            trace(currentLabel) 
        } 
        override public function get currentLabel():String 
        { 
            var str:String = "Override: "; 
            str += super.currentLabel; 
            return str; 
        } 
    } 
}

El resultado de la sentencia trace() en el constructor de la clase OverrideExample es Override: null, lo que indica que el ejemplo pudo sustituir la propiedad heredada currentLabel.

Propiedades estáticas no heredadas

Las subclases no heredan las propiedades estáticas. Esto significa que no se puede acceder a las propiedades estáticas a través de una instancia de una subclase. Sólo se puede acceder a una propiedad estática a través del objeto de clase en el que está definida. Por ejemplo, en el código siguiente se define una clase base denominada Base y una subclase que amplía Base denominada Extender. Se define una variable estática denominada test en la clase Base. El código del siguiente fragmento no se compila en modo estricto y genera un error en tiempo de ejecución en modo estándar.

package { 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
            trace(myExt.test);// error 
        } 
    } 
} 
 
class Base { 
    public static var test:String = "static"; 
} 
 
class Extender extends Base { }

La única manera de acceder a la variable estática test es a través del objeto de clase, como se indica en el código siguiente:

Base.test;

No obstante, se puede definir una propiedad de instancia con el mismo nombre que una propiedad estática. Esta propiedad de instancia puede definirse en la misma clase que la propiedad estática o en una subclase. Por ejemplo, la clase Base del ejemplo anterior podría tener una propiedad de instancia denominada test. El código siguiente se compila y ejecuta, ya que la propiedad de instancia es heredada por la clase Extender. El código también se compilará y ejecutará si la definición de la variable de instancia de prueba se mueve (en lugar de copiarse) a la clase Extender.

package 
{ 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
            trace(myExt.test);// output: instance 
        } 
    } 
} 
 
class Base 
{ 
    public static var test:String = "static"; 
    public var test:String = "instance"; 
} 
 
class Extender extends Base {}

Propiedades estáticas y cadena de ámbitos

Aunque las propiedades estáticas no se heredan, están en la cadena de ámbitos de la clase en la que se definen y de cualquier subclase de dicha clase. Así, se dice que las propiedades estáticas están dentro del ámbito de la clase en la que se definen y de sus subclases. Esto significa que una propiedad estática es directamente accesible en el cuerpo de la clase en la que se define y en cualquier subclase de dicha clase.

En el ejemplo siguiente se modifican las clases definidas en el ejemplo anterior para mostrar que la variable estática test definida en la clase Base está en el ámbito de la clase Extender. Es decir, la clase Extender puede acceder a la variable estática test sin añadir a la variable el nombre de la clase que define test como prefijo.

package 
{ 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
        } 
    } 
} 
 
class Base { 
    public static var test:String = "static"; 
} 
 
class Extender extends Base 
{ 
    public function Extender() 
    { 
        trace(test); // output: static 
    } 
     
}

Si se define una propiedad de instancia que utiliza el mismo nombre que una propiedad estática en la misma clase o en una superclase, la propiedad de instancia tiene precedencia superior en la cadena de ámbitos. Se dice que la propiedad de instancia oculta la propiedad estática, lo que significa que se utiliza el valor de la propiedad de instancia en lugar del valor de la propiedad estática. Por ejemplo, el código siguiente muestra que si la clase Extender define una variable de instancia denominada test, la sentencia trace() utiliza el valor de la variable de instancia en lugar del valor de la variable estática:

package 
{ 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
        } 
    } 
} 
 
class Base 
{ 
    public static var test:String = "static"; 
} 
 
class Extender extends Base 
{ 
    public var test:String = "instance"; 
    public function Extender() 
    { 
        trace(test); // output: instance 
    } 
     
}