Bienvenidos sean a este post, hoy seguiremos con otro de los pilares de OOP.
La herencia es la capaciddad que tenemos para poder transmitir caracteristicas de un elemento a otro, es decir heredarlas. Esto se aplica en clases e interfaces, siempre se transmite de un elemento madre a sus hijas, ya sean clases o interfaces, tambien es siempre hacia abajo. Una hija puede ser heredera de otra y en ese caso la superior pasara a ser madre de esta pero seguira siendo hija de su madre (aunque suene feo) y automaticamente las hijas tienen las propiedades y metodos de la madre y a su vez estas pueden terner sus propios metodo y propiedades. Tambien existen otras particularidades pero mas adelante las mencionaremos, comencemos con la herencia en interfaces.
Para hablar sobre este tipo de herencias analicemos el siguiente codigo:
interface IAnimal {
id: number;
}
interface IMamifero extends IAnimal {
peso: number;
altura: number;
}
let arturo: IMamifero = {
id: 1,
peso: 30,
altura: 50
}
console.log("id: " + arturo.id);
console.log("peso: " + arturo.peso);
console.log("altura: " + arturo.altura);
La primera interfaz sera nuestra interfaz madre la cual contendra solo una propiedad. La siguiente interfaz sera la hija de la anterior y para ello usaremos la palabra extends seguida del nombre de la madre y en esta tendremos dos propiedades mas. Con esto ya tenemos a la heredera de IAnimal. Nuestro siguiente paso sera crear un objeto de esta ultima interfaz, para ello la crearemos como siempre y pasaremos como tipo el nombre la interfaz para despues no solo almacenar los valores de esta sino tambien el de la clase madre. Para finalmente mostrar los tres valores, compilemos y veamos como es su salida:
$ node herencia.js
id: 1
peso: 30
altura: 50
$
A esto nos referimos con la herencia, una vez pasado la interfaz hija tendra acceso indiscriminado a los de la madre pero tambien podemos tener mas de una herencia, veamos el siguiente ejemplo:
interface IMadre {
id: number;
}
interface IPadre {
nombre: string;
}
interface IHija extends IMadre, IPadre {
desc: string;
}
let objeto: IHija = {
id: 1,
nombre: "Marta",
desc: "Es de prueba"
}
Este es muy similar al anterior pero ahora tendremos dos interfaces bases, IMadre e IPadre, para las cuales IHija sera heredera de estas, para ello seguimos usando extends y mediante la coma pasamos la otra clase, y asi podemos pasar todas las interfaces necesarias. Esto se lo denomina como herencia multiple, por ultimo creamos un objeto donde estableceremos todos los valores que posee esta ultima interfaz, tal como vimos anteriormente.
Recien vimos como implemenntar herencia para interfaces, ahora veremos lo mismo pero para clases. Tomemos el codigo anterior y realicemos la siguiente modificacion:
class Animal {
id: number | undefined;
}
class Mamifero extends Animal {
peso: number | undefined;
altura: number | undefined;
}
class Perro extends Mamifero {
nombre: string | undefined;
mostrar(): void {
console.log("id: " + this.id);
console.log("peso: " + this.peso);
console.log("altura: " + this.altura);
console.log("nombre: " + this.nombre);
};
}
let arturo = new Perro();
arturo.id = 1;
arturo.peso = 30;
arturo.altura = 50;
arturo.nombre = "Arturo";
arturo.mostrar();
Este es muy similar a las interfaces pero en lugar de estas usaremos clases. Primer detalle que agregamos es la union de tipos para poder utilizarlos, dado que no tenemos un constructor no podemos tenerlas sin iniciar, la estructura es muy similar donde tenemos una base o madre llamada Animal con una propiedad. La siguiente es Mamifero que sera heredera o hija de la anterior y tendra dos propiedades mas. Siguiendo a esta tenemos una tercera llamada Perro que sera heredera de la anterior y tambien de la primera. En esta no solo tendremos una propiedad mas sino tambien un metodo para mostrar todos los valores de las propiedades propias y heredadas. Para finalmente crear un objeto de esta clase, definir los valores de cada una de las propiedades y llamar al metodo para ver cada uno de ellos. Compilemos y veamos como es su salida:
$ node herencia.js
id: 1
peso: 30
altura: 50
nombre: Arturo
$
Logramos el objetivo, en la ultima clase tenemos todas las propiedades anteriores y pudimos asignarles un valor. Antes de pasar al siguiente tema debemos comentar que las clases no poseen herencia multiple como si tiene interface. Pero es una verdad a medias porque las clases si tienen herencia multiple pero si lo hacemos mediante interfaces.
Seguramente se estaran preguntando porque no implementamos un constructor para la clase anterior? Esto tiene una explicacion pero esta relacionada con el tema que debemos tratar ahora, para ello tomemos el codigo anterior y realicemos la siguiente modificacion:
class Animal {
id: number;
constructor(id: number) {
this.id = id;
}
}
class Mamifero extends Animal {
peso: number;
altura: number;
constructor(id: number, peso: number, altura: number) {
super(id);
this.peso = peso;
this.altura = altura;
}
}
class Perro extends Mamifero {
nombre: string;
constructor(id: number, peso: number,
altura: number, nombre: string) {
super(id, peso, altura);
this.nombre = nombre;
};
mostrar(): void {
console.log("id: " + this.id);
console.log("peso: " + this.peso);
console.log("altura: " + this.altura);
console.log("nombre: " + this.nombre);
};
}
let arturo = new Perro(1, 30, 50, "arturo");
arturo.mostrar();
La estructura es igual a la vista anteriormente pero ahora agregamos los constructores para cada clase. Esto es asi porque entra en accion una palabra clave como es super, la cual nos permite pasar datos al constructor de la clase madre. Tomemos este codigo, la primer clase no tiene ningun llamado a super porque no es heredera de nadie. En cambio la segunda clase tiene un constructor con los dos argumentos de las propiedades de esta y el valor de propiedad de la clase anterior. Para este constructor utilizamos un super para enviar el dato de id. Los otros dos son paraa iniciar a las propiedades internas. En la siguiente clase, el constructor sera el que utilicemos por lo tanto debe recibir los valores para las propiedades de sus clases madres y la propia. Para este caso el super enviara los valores de cada propiedad de las otras clases pero deben respetar el mismo orden del constructor anterior y despues estableceremos la propiedad de esta clase. El metodo sigue siendo el mismo y lo unico que cambiamos es la creacion del objeto donde ahora le pasaremos todos los datos y luego llamaremos al metodo. Compilemos y veamos la salida:
$ node herencia.js
id: 1
peso: 30
altura: 50
nombre: Arturo
$
Con esto comentado podemos pasar al siguiente tema como es la anulacion (overriding) de funciones. Esta mecanica nos permite reemplazar los metodos heredados por acciones mas acordes a nuestras clases. Vamos a analizar el siguiente codigo:
class Animal {
id: number | undefined;
}
class Mamifero extends Animal {
peso: number | undefined;
altura: number | undefined;
hablar(): void {
console.log("El mamifero habla");
};
}
class Perro extends Mamifero {
nombre: string | undefined;
hablar(): void {
console.log(this.nombre + " dice Guau!");
};
}
let mammal = new Mamifero();
let arturo = new Perro();
arturo.nombre = "Arturo";
mammal.hablar();
arturo.hablar();
Para facilitar el codigo eliminamos a los constructores y agregamos en la clase mamifero al metodo hablar. Este nuevo metodo nos notificara que el mamifero esta hablando sin identificar como exactamente porque es un animal muy generico. En la clase perro tambien eliminamos al metodo mostrar, solo para facilitar el codigo, y en este tenemos al metodo hablar pero a diferencia del anterior este muestra otro mensaje. Seguramente estaran pensando que el override es igual al overload, es una verdad a medias porque tienen coincidencias pero no muchas.
Un overload o sobrecarga mantiene el mismo nombre de la funcion pero varia su firma (signature) que es establecida mediante los argumentos que le pasamos, en el caso del overload las nuevas funciones deben tener argumentos con distintos tipos, el bloque de codigo no influye en este caso donde podemos tener el mismo codigo en cada nueva funcion. En cambio el override o anulacion debe tener la misma firma, es decir el mismo nombre y los mismos argumentos pero si cambia el codigo en el bloque donde debe ser distinto al de la clase madre o base. En el caso de nuestro codigo son similares pero al tener mensajes distintos el codigo lo da como valido.
Siguiendo con este creamos dos objetos, el primero sera de tipo Mamifero y el segundo de tipo Perro. Al segundo le establecemos la propiedad nombre para finalmente llamar a los metodos hablar de cada clase. Compilemos y veamos como es la salida:
$ node herencia.js
El mamifero habla
Arturo dice Guau!
$
Observen como ahora cada clase tiene su propia version del mismo metodo para diferenciar cada una de las mismas. Si existiera otra clase heredera de Mamifero o Perro se puede volver a hacer un override sobre este metodo, esto lo podemos hacer todas las veces que necesitemos siempre y cuando respetemos lo comentado anteriormente.
Si vienen de posts anteriores, hemos mencionado a los modificadores de acceso y hemos visto/trabajado con tres de los cuatro disponibles (public, private y readonly) pero no hemos mencionado nada de protected. Este modificador es muy particular porque protege los datos como si fuera privado para cuando queremos acceder desde afuera de la clase pero estos mismos seran publicos si accedemos desde una clase heredera de la misma. Para entender este concepto analicemos el siguiente codigo:
class Animal {
protected id: number | undefined;
}
class Mamifero extends Animal {
peso: number | undefined;
altura: number | undefined;
}
class Perro extends Mamifero {
nombre: string | undefined;
mostrar(): void {
console.log("id: " + this.id);
console.log("nombre: " + this.nombre);
};
}
let arturo = new Perro();
arturo.id = 1;
arturo.nombre = "Arturo";
arturo.mostrar();
Seguimos con la misma estructura anterior pero sin los metodos hablar y un par de simples modificaciones, la primera es que modificamos al id de la clase Animal y le establecimos el protected. La siguiente modificacion es que en el metodo de la clase Perro mostraremos a id y nombre. Por ultimo creamos el objeto, establecemos el valor para id y nombre y llamamos a mostrar. Compilemos y veamos la salida:
$ tsc
herencia.ts:17:8 - error TS2445: Property 'id' is protected and only accessible within class 'Animal' and its subclasses.
17 arturo.id = 1;
~~
Found 1 error in herencia.ts:17
$
Ocurrio lo que comentamos anteriormente, esta propiedad al estar protegida la podemos manipular internamente o con sus hijas. Tomemos el codigo anterior y realicemos la siguiente modificacion:
class Animal {
protected id: number | undefined;
}
class Mamifero extends Animal {
peso: number | undefined;
altura: number | undefined;
}
class Perro extends Mamifero {
nombre: string | undefined;
setId(i: number) {
this.id = i;
}
mostrar(): void {
console.log("id: " + this.id);
console.log("nombre: " + this.nombre);
};
}
let arturo = new Perro();
arturo.setId(1);
arturo.nombre = "Arturo";
arturo.mostrar();
La primera modificacion la realizamos en la clase Perro donde establecimos un nuevo metodo llamado setId que recibe un valor y lo asigna a id. La siguiente fue a la hora de asignar el id, para ello hacemos el llamado al nuevo metodo y le pasamos el valor. El resto del codigo sigue igual, compilemos y veamos como es la salida:
$ node herencia.js
id: 1
nombre: Arturo
$
Como pueden ver funciono perfectamente porque el metodo para establecerlo y mostrarlo esta en un clase heredera. Es una buena forma de conceder un acceso mas limitado sin llegar al extremo de la restriccion total, por eso usualmente lo encuentran en muchos codigos.
Nuestro siguiente paso sera hablar sobre las clases abstractas. Estas son una particularidad porque no solamente establecen una estructura para ser utilizada en distintas clases sino que no pueden ser instanciadas, es decir crear objetos a partir de ellas, por lo tanto todo lo que definamos en ellas solo podra ser accedida mediante las herederas de esta. Esto nos da la posibilidad de poder establecer los parametros base que no necesitamos que sean de dominio publico bajo ninguna circunstancia. Pasemos a analizar el siguiente codigo:
abstract class Animal {
id: number;
peso: number;
altura: number;
constructor(id: number, peso: number, altura: number) {
this.id = id;
this.peso = peso;
this.altura = altura;
}
}
class Mamifero extends Animal { }
class Perro extends Mamifero {
nombre: string | undefined;
mostrar(): void {
console.log("id: " + this.id);
console.log("peso: " + this.peso);
console.log("altura: " + this.altura);
console.log("nombre: " + this.nombre);
}
}
let arturo = new Perro(1, 30, 50);
arturo.nombre = "Arturo";
arturo.mostrar();
Seguimos con nuestro codigo pero con varias modificaciones. La primera y mas evidente que establecimos a Animal como abstracta y ahora contendra todos las propiedades basicas y un constructor para iniciarlas. Como las propiedades de Mamifero las pasamos a Animal podriamos eliminarlo pero lo vamos a dejar en blanco y que siga como heredero de Animal. Para despues tener a Perro como heredero de Mamifero donde tenemos una propiedad identificada como nombre y el metodo mostrar con todas las propiedades propias y heredadas. Lo siguiente es crear el objeto de la ultima clase, observen que pasamos los datos para el constructor de Animal, luego establecemos el nombre y por ultimo llamamos a mostrar. Con esto comentado, compilemos y veamos que sucede:
$ node herencia.js
id: 1
peso: 30
altura: 50
nombre: Arturo
$
Funciono perfectamente, heredamos todas las propiedades de la clase abstracta mediante una hija de la misma y a su vez pudimos no solo usar el constructor sino tambien pudimos acceder a ellas. En el codigo anterior despues de las clases agreguen la siguiente linea:
let animal = new Animal(1, 30, 50);
Esto intentara crear un objeto de la clase abstracta, si lo compilan les devolvera lo siguiente:
$ tsc
herencia.ts:22:14 - error TS2511: Cannot create an instance of an abstract class.
22 let animal = new Animal(1, 30, 50);
~~~~~~~~~~~~~~~~~~~~~
Found 1 error in herencia.ts:22
$
Lo que comentamos anteriormente, si no se percataron anteriormente cuando hablamos de overriding de metodos en el ejemplo creamos un objeto de Mamifero para ver como se anulaba al metodo en Perro. En todos los codigos anteriores podiamos hacer objetos de cualquiera de las clases disponibles, en cambio en este codigo podemos hacer de Mamifero y Perro pero no de Animal, para eso sirve principalmente abstract.
Nuestro siguiente tema es el metodo abstracto de una clase, puede ser similar hasta un punto. En este caso nos servira como prototipo y en la clase o clases herederas debemos anularlo u overriding. Para entender el concepto vamos a tomar el codigo anterior y realicemos el siguiente cambio:
abstract class Animal {
id: number;
peso: number;
altura: number;
abstract mostrar(): void;
constructor(id: number, peso: number, altura: number) {
this.id = id;
this.peso = peso;
this.altura = altura;
}
}
class Mamifero extends Animal {
mostrar(): void {
console.log("Mamufero.id: " + this.id);
console.log("Mamufero.peso: " + this.peso);
console.log("Mamufero.altura: " + this.altura);
}
}
class Perro extends Mamifero {
nombre: string | undefined;
mostrar(): void {
console.log("Perro.id: " + this.id);
console.log("Perro.peso: " + this.peso);
console.log("Perro.altura: " + this.altura);
console.log("Perro.nombre: " + this.nombre);
}
}
let animal = new Mamifero(1, 50, 100);
let arturo = new Perro(2, 30, 50);
arturo.nombre = "Arturo";
animal.mostrar();
arturo.mostrar();
Seguimos con nuestra clase Animal como abstracta y todas las mismas propiedades y el constructor pero ahora agregamos un metodo mostrar como abstracto. Simplemente lleva la palabra abstract y el tipo que devuelve pero sin ninguna definicion. La siguiente modificacion es en la clase Mamifero donde ahora si definimos al metodo mostrar y en ella mostraremos los valores heredados, este es nuestro primer overridde. El segundo sera en la clase Perro pero es la misma definicion que teniamos antes. Por ultimo crearemos un nuevo objeto pero de Mamifero, le cargaremos unos valores y luego llamaremos al metodos mostrar de cada objeto. Compilemos y veamos como es la salida:
tinchicus@dbn001vrt:~/lenguajes/ts/22$ node herencia.js
Mamufero.id: 1
Mamufero.peso: 50
Mamufero.altura: 100
Perro.id: 2
Perro.peso: 30
Perro.altura: 50
Perro.nombre: Arturo
tinchicus@dbn001vrt:~/lenguajes/ts/22$
Como vinimos trabajando hasta ahora, esta forma de trabajar es muy util para cuando debemos utilizar un metodo en multiples clases pero puede que no se haya declarado una antes que la otra y de esta forma nos aseguramos que este disponible para el momento de llamarla. Por ahi en este lenguaje no sea tan evidente pero en otros como Java o C# son muy utiles y practicas.
Tambien disponemos de una herramienta para poder averiguar si un objeto es una instancia o derivado de una clase en particular. Esta funcion es instanceof y este nos devolvera un valor booleano. Analicemos el sigueinte codigo para verlo en accion:
abstract class Animal {
id: number | undefined;
peso: number | undefined;
altura: number | undefined;
}
class Mamifero extends Animal {}
class Perro extends Mamifero {
nombre: string | undefined;
mostrar(): void {
console.log("Perro.id: " + this.id);
console.log("Perro.peso: " + this.peso);
console.log("Perro.altura: " + this.altura);
console.log("Perro.nombre: " + this.nombre);
}
}
console.log("Mamifero es hija de Animal: "
+ (new Mamifero() instanceof Animal));
console.log("Mamifero es hija de Perro: "
+ (new Mamifero() instanceof Perro));
console.log("Perro es hija de Animal: "
+ (new Perro() instanceof Animal));
Aqui limpiamos un poco el codigo para tenerlo un poco mas sencillo. En este caso seguimos con las herencias de Animal a Mamifero a Perro. El resto es similar a lo que vinimos viendo hasta ahora salvo en el final. En este mostraremos tres mensajes para cada comparacion y en cada uno de ellos aplicaremos a instanceof, tomemos uno de ejemplo para analizarlo:
new Mamifero() instanceof Animal
La primera parte creamos la instancia de Mamifero mediante el new, seguido del instanceof y luego otra clase para averiguar si lo anterior es heredera de esta. En este caso le pasamos Animal para que nos devuelva esta verificacion. En el siguiente caso es lo mismo pero lo hacemos contra Perro y en el ultimo caso lo hacemos entre Perro y Animal. Compilemos y veamos que nos muestra en la salida:
$ node herencia.js
Mamifero es hija de Animal: true
Mamifero es hija de Perro: false
Perro es hija de Animal: true
$
Observen lo que nos devolvio, en el primer caso es true porque efectivamente Mamifero es hija de Animal. En el segundo caso es false porque Mamifero no es heredera de Perro sino mas bien al reves y en el ultimo caso es true porque si bien Perro no es heredero directo de Animal si lo es mediante Mamifero por lo tanto es de forma indirecta pero lo es. Es una herramienta muy practica por si necesitamos establecer alguna verificacion de este estilo. Antes de finalizar veremos como se implementan interfaces junto a clases. Para ello tomaremos el codigo anterior y lo modificaremos de la siguiente manera:
class Animal {
id: number | undefined;
mostrar(): void {
console.log("id: " + this.id);
}
}
interface IMamifero extends Animal {
setId(id: number): void;
}
class Perro extends Animal implements IMamifero {
setId(id: number): void {
this.id = id;
}
}
let arturo = new Perro();
arturo.setId(1);
arturo.mostrar();
En este codigo tenemos la clase Animal que seguira siendo la madre, aqui tenemos una propiedad y un metodo para mostrarlo. Ya no tendremos mas la clase Mamifero sino una interfaz que sera heredera de la clase Anterior y aqui tenemos un prototipo de un metodo. Luego tenemos una clase que sera heredera de Animal pero a su vez implementa a la interfaz anterior y aqui definiremos al metodo de la interfaz. Por ultimo creamos un objeto, usamos a setId para establecer un valor para id y luego verlo mediante mostrar. Compilemos y veamos la salida:
$ node herencia.js
id: 1
$
El codigo funciono con todo lo que hicimos en el codigo. Una particularidad que podemos mencionar es que de la forma que implementamos a la interfaz en la clase tambien podemos implementar varias interfaces para tener algo similar a herencia multiple donde si bien no tendremos metodos y/o propiedades si tendremos prototipos y los podremos definir en las clases.
En resumen, hoy hemos visto herencia, que es, para que sirve, como se utiliza las distintas formas para implementarlo, asi como tambien algunas herramientas utiles para trabajar con distintas formas entre estas, espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.50
