Herencia


En la mayoría de los lenguajes de programación la herencia es un tema de gran importancia.

En lenguajes clásicos (como Java), la herencia (o extender) proporciona dos servicios útiles. En primer lugar, se trata de una forma de reutilización de código. Si una nueva clase es en su mayoría similar a una clase existente, sólo tienes que especificar las diferencias. Los patrones de reutilización de código también son muy importantes porque tienen el potencial de reducir significativamente los costos en el desarrollo de software. Otra de las ventajas de la herencia clásica es que incluye la especificación de un sistema de tipos. Esto libera sobre todo al programador de tener que escribir operaciones de casting  explícitas, lo cual es muy bueno porque cuando hace casting, los beneficios de seguridad de un sistema de tipo se pierden.

JavaScript es un lenguaje de programación debilmente tipado. El linaje de un objeto es irrelevante. Lo importante de un objeto es lo que puede hacer.

JavaScript proporciona una serie rica en patrones de reutilización de código. Puede imitar el patrón clásico, pero también es compatible con otros modelos que son más expresivos. El conjunto de posibles patrones de herencia en JavaScript es enorme. Vamos a ver algunos de los modelos más sencillos. Construcciones mucho más complejas son posibles, pero por lo general lo mejor es mantener la sencillez.

En los lenguajes clásicos, los objetos son instancias de clases y una clase puede heredar de otra clase. JavaScript es un lenguaje de prototipos, lo que significa que los objetos heredan directamente de otros objetos.

Pseudo-clásico

JavaScript está en conflicto por su naturaleza prototípica. Su mecanismo de prototipos está oscurecido por una complicada empresa sintáctica que se parece vagamente a lo clásico. Un nivel innecesario de direccionamiento indirecto está insertado de tal manera que los objetos son producidos por funciones constructoras.

Cuando se crea un objeto de función, el constructor Function que produce el objeto de función ejecuta algún código como este:

this.prototype = {constructor: this};

El nuevo objeto de la función recibe una propiedad prototype cuyo valor es un objeto que contiene una propiedad constructor cuyo valor es el nuevo objeto de función. El objeto prototype es el lugar en donde los rasgos heredados van a ser depositados. Cada función obtiene un objeto prototype porque el lenguaje no proporciona una forma de determinar qué funciones están destinadas a ser utilizadas como constructoras. La propiedad constructor no es útil. El objeto prototype es el que importa.

Cuando una función es invocada con el patrón de invocación del constructor usando el prefijo new, este modifica la forma en la que se ejecuta la función. Si el operador new fuera un método en lugar de un operador, podría haber sido implementado de esta manera:

Function.method('new', function ( ) {
    // Crea un nuevo objeto que hereda desde el prototipo
    // del constructor.

    var that = Object.create(this.prototype);

    // Invoca al constructor, y vincula –this- al
    // nuevo objeto.

    var otro = this.apply(that, arguments);

    // Si el valor devuelto no es un objeto,
    // sustituye el nuevo objeto.

     return (typeof otro === 'object' && otro) || that;
});

Podemos definir un constructor y ampliar su prototipo:

var Mamifero = function (nombre) {
    this.nombre = nombre;
};

Mamifero.prototype.obtener_nombre = function ( ) {
    return this.nombre;
};

Mamifero.prototype.dice = function ( ) {
    return this.diciendo || '';
};

Ahora podemos crear una instancia:

var miMamifero = new Mamifero('Hierbas de mamiferos');
var nombre = miMamifero.obtener_nombre( ); // 'Hierbas de mamiferos'

Podemos hacer otra pseudoclase que hereda de Mamifero mediante la definición de su función constructora y reemplazar su prototype con una instancia de Mamifero:

var Gato = function (nombre) {
    this.nombre = nombre;
    this.diciendo = 'miau';
};

// Reemplazamos Gato.prototype con una nueva instancia de Mamifero

Gato.prototype = new Mamifero( );

// Ampliamos el prototipo nuevo con
// los métodos ronronea and obtener_nombre.

Gato.prototype.ronronea = function (n) {
    var i, s = '';
    for (i = 0; i < n; i += 1) {
        if (s) {
            s += '-';
        }
        s += 'r';
    }
    return s;
};

Gato.prototype.obtener_nombre = function ( ) {
    return this.dice( ) + ' ' + this.nombre +
            ' ' + this.dice( );
};

var miGato = new Gato('Diana');
var dice = miGato.dice( );               // 'miau'
var ronronea = miGato.ronronea(5);       // 'r-r-r-r-r'
var nombre = miGato.obtener_nombre( );   // 'miau Diana miau'

El patrón pseudoclasico pretende lucir como oriendado a objetos, pero esto se ve bastante extraño. Podemos ocultar algo de fealdad utilizando el método method y definiendo un método heredado:

Function.method('inherits', function (Parent) {
    this.prototype = new Parent( );
    return this;
});

Nuestras herencias y método method retornan this, lo que nos permite programar en un estilo de cascada. Ahora podemos hacer Gato con una declaración.

var Gato = function (nombre) {
    this.nombre = nombre;
    this.diciendo = 'miau';
}.
    inherits(Mamifero).
    method('ronronea', function (n) {
        var i, s = '';
        for (i = 0; i < n; i += 1) {
            if (s) {
            s += '-';
        }        
        s += 'r';
    } 
    return s;
    }).
    method('obtener_nombre', function ( ) {
        return this.dice( ) + ' ' + this.nombre +
                ' ' + this.dice( );
    });

Al ocultar el prototype jazz, ahora parece un poco menos extraño. Pero ¿realmente hemos mejorado algo? Ahora tenemos funciones constructoras que actúan como clases, pero en los extremos, puede haber un comportamiento sorprendente. No hay privacidad; todas las propiedades son públicas. No hay acceso a los métodos super.

Lo que es peor, hay un peligro grave con el uso de las funciones constructoras. Si te olvidas de incluir el prefijo new al llamar a una función constructora, entonces esto no podrá enlazar a un objeto nuevo. Lamentablemente, this se enlaza con el objeto global, así que en vez de ampliar el nuevo objeto, sobreescribirás las variables globales. Eso es realmente malo. No hay ninguna advertencia en la compilación, y no hay ninguna advertencia en tiempo de ejecución.

Esto es un grave error en el diseño del lenguaje. Para mitigar este problema, hay una convención de que todas las funciones constructoras se nombren con una capital inicial, y que ninguna otra cosa se escriba con un capital inicial. Y con esto ponernos a rezar para que con una inspección visual podamos encontrar un new perdido. Una alternativa mucho mejor es no utilizar en absoluto new.

La forma pseudo-clasica proporciona comodidad a los programadores que no están familiarizados con el lenguaje JavaScript, pero también oculta la verdadera naturaleza del lenguaje. La notación de inspiración clásica puede inducir a los programadores a componer jerarquías innecesariamente profundas y complicadas. Gran parte de la complejidad de las jerarquías de clase estan motivadas por las limitaciones del control de tipo estático. JavaScript es completamente libre de esas limitaciones. En el lenguaje clásico, la herencia de clases es la única forma de reutilización de código. JavaScript tiene más y mejores opciones.

Especificadores de objeto

A veces sucede que a un constructor se le da un gran número de parámetros. Esto puede ser problemático, ya que puede ser muy difícil de recordar el orden de los argumentos. En tal caso, puede ser mucho más agradable si escribimos el constructor para que acepte un único objeto especifico. Este objeto contiene la especificación del objeto a ser construido. Así, en lugar de:

var miObjeto = creador(f, l, m, c, s);

podemos escribir:

var miObjeto = mcreador({
    primero: f,
    ultimo: l,
    estado: s,
    ciudad: c
});

Los argumentos ahora pueden enumerarse en cualquier orden, los argumentos pueden omitirse si el constructor es inteligente en relación con los valores predeterminados, y el código es mucho más fácil de leer.

Esto puede tener un beneficio secundario al trabajar con JSON. El texto JSON sólo puede describir datos, pero a veces los datos representan un objeto, y sería útil para asociar los datos con sus métodos. Esto se puede hacer trivialmente si el constructor toma un especificador de objeto porque podemos simplemente pasar el objeto JSON al constructor y se regresará un objeto totalmente constituido.

Prototypal

En un patrón puramente prototípico, prescindimos de las clases. Más bien nos enfocamos en los objetos. La herencia de prototipos es conceptualmente más simple que la herencia clásica: un objeto nuevo puede heredar las propiedades de un objeto antiguo. Esto tal vez no sea familiar, pero es muy fácil de entender. Se comienza por hacer un objeto útil. Luego, puedes hacer muchos más objetos que sean similares a este. El proceso de clasificación de romper una aplicación en un conjunto de clases abstractas anidadas puede ser evitado por completo.

Comencemos con un objeto literal para hacer un objeto útil:

var miMamifero = {
    nombre : 'Hierbas de Mamifero',
    obtener_nombre : function ( ) {
        return this.nombre;
    },
    dice : function ( ) {
         return this.diciendo || '';
    }
};

Una vez que tenemos un objeto que nos gusta, podemos hacer más instancias con el método Object.create que vimos anteriormente. Podemos personalizar las nuevas instancias:

var miGato = Object.create(miMamifero);
miGato.nombre = 'Diana';
miGato.diciendo = 'miau';

miGato.ronronea = function (n) {
    var i, s = '';
    for (i = 0; i < n; i += 1) {
        if (s) {
            s += '-';
        }
        s += 'r';
    }

    return s;
};

miGato.obtener_nombre = function ( ) {
    return this.dice( ) + ' ' + this.nombre + ' ' + this.dice( );
};

Esta es la herencia diferencial. Personalizando un objeto nuevo, especificamos las diferencias del objeto sobre el cual se basa.

A veces es útil para las estructuras de datos para heredar de otras estructuras de datos. He aquí un ejemplo: Supongamos que se está analizando un lenguaje como JavaScript o TEX en el que un par de llaves indica un ámbito. Los elementos que se definen en un ámbito no son visibles fuera del ámbito. En cierto sentido, un ámbito interno hereda de su ámbito externo. Los objetos JavaScript son muy buenos en lo que representa esta relación. El bloque de función se llama cuando se encuentra una llave izquierda. La función de análisis buscará símbolos desde su alcance, y amplia el alcance cuando define nuevos símbolos:

var bloque = function ( ) {

    // Recuerda el ámbito actual. Crea un nuevo ámbito
    // que incluye todo desde el actual.

    var alcanceViejo = alcance;
    alcance = Object.create(alcance);

    // Avanza más allá de la llave de apertura.

    avanzar('{');

    // Analizar usando del nuevo ámbito.

    parse(alcance);

    // Avanzar más allá de la llave de cierre y desecha
    / el nuevo ámbito, restaurando el antiguo.

    avanzar('}');
    alcance = alcanceViejo;
};

Funcional

Una de las debilidades de los patrones de herencia que hemos visto hasta ahora es que no tenemos privacidad. Todas las propiedades de un objeto son visibles. No recibimos variables privadas y no hay métodos privados. A veces esto no importa, pero a veces importa mucho. En la frustración, algunos programadores poco informados han adoptado un patrón de privacidad simulada. Si tienes una propiedad que deseas hacer privada, podrías darle un nombre con aspecto extraño, con la esperanza de que otros usuarios del código pretenderán que no pueden ver a los miembros de aspecto extraño. Afortunadamente, tenemos una alternativa mucho mejor en una aplicación de patrón de modulo.

Comenzaremos haciendo una función que va a producir objetos. Vamos a darle un nombre que empieza con una letra minúscula, ya que no requiere el uso del prefijo new. La función consta de cuatro pasos:

  1. Se crea un nuevo objeto. Hay un montón de maneras de crear un objeto. Se puede hacer un objeto literal, o puede llamar a una función constructora con el prefijo new, o puedes utilizar el método Object.create para hacer una nueva instancia de un objeto existente, o puedes llamar a cualquier función que devuelva un objeto.
  2. Defines opcionalmente variables de instancia y métodos privados. Estos son sólo vars comunes de la función.
  3. Se amplia ese nuevo objeto con métodos. Estos métodos tendrán un acceso privilegiado a los parámetros y la vars definidas en el segundo paso.
  4. Retorna ese nuevo objeto.

Aquí hay una plantilla de pseudocódigo para un constructor funcional:

var constructor = function (especifico, mi) {
    var that, otras variables de instancia privada;

    mi = mi || {};

    Agregamos variables y funciones compartidas a 'mi'

    that = un objeto nuevo;

    Agregamos metodos privilegiados para 'that'

    return that;
};

El objeto especifico contiene toda la información que el constructor necesita para hacer una instancia. El contenido de especifico podría ser copiado en variables privadas o transformados por otras funciones. O los métodos pueden tener acceso a la información de especifico cuando lo necesiten. (Una simplificación es sustituir especifico con un valor único. Esto es útil cuando el objeto que se está construyendo no necesita un objeto especifico entero).

El objeto mi es un contenedor de secretos que son compartidos por los constructores en la cadena de herencia. El uso del objeto mi es opcional. Si el objeto mi no se pasa, entonces el objeto mi es creado.

A continuación, se declaran las variables de instancia privada y métodos privados para el objeto. Esto se hace simplemente declarando las variables. Las variables y funciones internas del constructor se convierten en los miembros privados de la instancia. Las funciones internas tienen acceso a especifico y mi y that y las variables privadas.

A continuación, agregamos los secretos compartidos para el objeto mi. Esto se hace mediante una asignación:

mi.miembro = valor;

Ahora, hacemos un nuevo objeto y lo asignamos a that. Hay un montón de maneras de hacer que un nuevo objeto. Podemos utilizar un literal de objeto. Podemos llamar a un constructor pseudo-clasico con el operador new. Podemos utilizar el método Object.create sobre un objeto prototype. O bien, podemos llamar a otro constructor funcional, pasándole el objeto de especifico (posiblemente el mismo objeto especifico que fue pasado a este constructor) y el objeto mi. El objeto mi le permite al otro constructor compartir el material que hemos puesto en mi. El otro constructor también puede poner sus propios secretos compartidos en mi para que nuestro constructor puede tomar ventaja de ello.

A continuación, ampliamos that, agregando los métodos privilegiados que componen la interfaz del objeto. Podemos asignar nuevas funciones a los miembros de that. O, de forma más segura, podemos definir las primeras funciones como métodos privados, y entonces las asignamos a that:

var metodico = function ( ) {
    ...
};
that.metodico = metodico;

La ventaja de definir metodico en dos pasos es que si otros métodos quieren llamar a metodico, ellos pueden llamar a metodico( ) en lugar de that.metodico( ). Si la instancia está dañada o alterada entonces that.metodico se sustituye, los métodos que llaman a metodico seguirán trabajando debido a que la privacidad de metodico no se ve afectada por la modificación de la instancia.

Finalmente, retornamos that.

Vamos a aplicar este patrón a nuestro ejemplo mamifero. Aquí no necesitamos a mi, así que vamos a dejarlo así, pero vamos a utilizar el objeto especifico.

Las propiedades nombre y dice son ahora completamente privadas. Ellas sólo son accesibles a través de los métodos privilegiados obtener_nombre y dice:

var mamifero = function (especifico) {
    var that = {};
    
   that.obtener_nombre = function ( ) {
        return especifico.nombre;
    };

    that.dice = function ( ) {
        return especifico.diciendo || '';
    };

    return that;

};

var miMamifero = mamifero({nombre: 'Hierba'});


En el patrón pseudo-clasico, la función constructora gato tuvo que duplicar el trabajo que fue hecho por el constructor mamifero. Eso no es necesario en el patrón funcional porque el constructor gato llamará al constructor mamifero, dejando que mamifero haga la mayoría del trabajo en la creación de objetos, por lo que el gato sólo tiene que preocuparse de las diferencias:

var gato = function (especifico) {
    especifico.diciendo = especifico.diciendo || 'miau';
    var that = mamifero(especifico);
    that.ronronea = function (n) {
        var i, s = '';
        for (i = 0; i < n; i += 1) {
            if (s) {
                s += '-';
            }
            s += 'r';
        }
        return s;
    };
    that.obtener_nombre = function ( ) {
        return that.dice( ) + ' ' + especifico.nombre +
        ' ' + that.dice( );
        return that;
    };
var miGato = gato({nombre: 'Diana'});

El patrón funcional también nos da una forma de lidiar con los métodos super. Crearemos un método superior que toma un nombre de método y devuelve una función que llama a ese método. La función invocará al método original incluso si la propiedad se cambia:

Object.method('superior', function (nombre) {
    var that = this,
    method = that[nombre];
    return function ( ) {
        return method.apply(that, arguments);
    };
});

Vamos a intentar sobre gatolindo que es igual que gato a menos que tenga un método obtener_nombre más fresco que llama al método super. Se requiere un poco de preparación. Declararemos una variable super_obtener_nombre y le asignamos el resultado de invocar el método superior:

var gatolindo = function (especifico) {
    var that = gato(especifico),
    super_obtener_nombre = that.superior('obtener_nombre');

    that.obtener_nombre = function (n) {
        return 'como ' + super_obtener_nombre( ) + ' bebe';
    };

    return that;
};

var miGatoLindo = gatolindo({nombre: 'Jake'});
var nombre = miGatoLindo.obtener_nombre( );

// 'como miau Jake miau bebe'

El patrón funcional tiene un alto grado de flexibilidad. Requiere menos esfuerzo que el patrón pseudo-clasico, y nos da una mejor encapsulación y ocultación de información y acceso a los métodos super.

Si todo el estado de un objeto es privado, entonces el objeto es a prueba de manipulaciones. Las propiedades del objeto pueden ser reemplazadas o eliminadas, pero la integridad del objeto no se ve comprometida. Si creamos un objeto en el estilo funcional, y si todos los métodos del objeto no hacen uso de this o that, entonces el objeto es durable. Un objeto durable es simplemente una colección de funciones que actúan como capacidades.

Un objeto duradero no puede ser comprometido. El acceso a un objeto duradero no le da a un atacante la posibilidad de acceder al estado interno del objeto con excepción de lo permitido por los métodos.

Partes

Podemos componer objetos a partir de un conjuntos de partes. Por ejemplo, podemos crear una función que pueda agregar características simples de procesamiento de eventos a cualquier objeto. Se agrega sobre un método, un método fuego, y un registro de evento privado:

var eventuality = function (that) {
    var registry = {};
        that.fire = function (event) {

            // Dispara un evento sobre un objeto. El evento puede
            // ser una cadena que contiene el nombre del evento o un objeto
            // objeto que contiene una propiedad de tipo que contiene el
            // nombre del evento. Los manejadores registrados por el
            // método "on" que coincide con el nombre del evento que será invocado

            var array,
            func,
            handler,
            i,
            type = typeof event === 'string' ?
                    event : event.type;

        // Si una matriz de manejadores existe para este evento, entonces
        // itera a través y ejecuta los manejadores en orden.

        if (registry.hasOwnProperty(type)) {
            array = registry[type];
            for (i = 0; i < array.length; i += 1) {
                handler = array[i];

        // Un registro del manejador contiene un método y una 
        // matriz opcional de parámetros. Si el método es un
        // nombre, buscar la función.

        func = handler.method;
        if (typeof func === 'string') {
            func = this[func];
        }

        
        // Invoca un manejador. Si el registro contiene los
        // parámetros, luego los pasa. De lo contrario,
        // pasa el objeto de evento.

        func.apply(this,
            handler.parameters || [event]);
            }
        }
        return this;
    };
    
    that.on = function (type, method, parameters) {

        // Registra un evento. Hace un registro del manejador. 
        // Lo pone en una matriz del manejador, haciendo uno
        // si todavía no existe para este tipo.

    var handler = {
        method: method,
        parameters: parameters
    };

    if (registry.hasOwnProperty(type)) {
        registry[type].push(handler);
    
    } else {
        registry[type] = [handler];
    }
    
    return this;
    };

    return that;
};

Podríamos llamar a eventuality sobre cualquier objeto individual, otorgando a métodos de manejo de eventos. También podríamos llamarlo en una función constructora antes de que sea devuelto:

eventuality(that);

De este modo, un constructor puede ensamblar objetos de un conjunto de partes. La tipificación suelta de JavaScript es una gran ventaja aquí porque no estamos agobiados con un sistema de tipo que está preocupado por el linaje de clases. En lugar de ello, nos podemos centrar en el carácter de su contenido.

Si quisieramos que eventuality tenga acceso al estado privada del objeto, podríamos pasarle el paquete my.

Añadir nuevo comentario

CAPTCHA
Esta pregunta es para comprobar si usted es un visitante humano y prevenir envíos de spam automatizado.
1 + 3 =
Resuelva este simple problema matemático y escriba la solución; por ejemplo: Para 1+3, escriba 4.