En todas partes se habla de herencia, pero se suelen centrar en sólo un tipo de herencia. Y por ello hoy queremos comentar un poco todo lo que conoces. La herencia no es igual en todos los lenguajes.

Tenemos diversos tipos de herencia, en función de las características de los lenguajes. En los lenguajes orientados a objetos, se heredan objetos. En los lenguajes basados en prototipos, se trabaja con el prototipo para modificarlo. En algunos lenguajes en vez de herencia tenemos interfaces y/o “traits”.

¿Qué es la herencia?

Es una forma de componer objetos/clases a partir de otras. Normalmente se suele tener sólo un único padre pero múltiples hijos. El uso más común de la herencia es utilizarla para poder tener diferente tipo de objetos con una interfaz común, o para particularizar comportamientos específicos.

Todos los hijos tiene lo mismo que el padre. Para esto hay que hablar de dos términos: covarianza y contravarianza. Lo más común suele ser la covarianza, si definimos Animal con dos subclases Cerdo y Perro. Una conjunto de cerdos lo podríamos ver cómo un conjunto de animales. Si lo vemos cómo un árbol, se puede ir hacia el padre siempre. También existe la contravianza, que es lo opuesto, si tenemos un conjunto de animales se podría pasar a tener un conjunto de cerdos.

Por otra parte, tenemos la invarianza, si es del tipo Animal, estamos buscado cosas del tipo Animal. Por ejemplo, queremos un conjunto de Animales, no buscamos otra cosa. Aunque podemos tener un conjunto de animales basado en cerdos y perros, aunque actuarán como animales, gracias a la covarianza.

Por último, se suele tener siempre un padre a cualquier objeto. En java tenemos Object, en python object, etcétera. Normalmente no se ve, pero siempre está ahí.

Herencia en los lenguajes orientados a objetos

El título indica claramente que vamos a trabajar con objetos. En este caso tenemos que conocer que hay objetos normales, y objetos abstractos. Los objetos abstractos son objetos que no podremos instanciar, es decir, que no vamos a poder crearlos pero si trabajarlos. Además las propiedades y funciones de los objetos pueden ser de tres tipos: públicas, privadas y protegidas. Las publicas, todo el mundo las puede ver; las privadas, sólo las puede ver el objeto que lo define; y las protegidas, son visibles para el objeto que los definen y los que hereden.

Existen dos casuísticas a tener en cuenta. Si el padre define un método o atributo, el hijo por defecto lo tendrá, pero lo podrá modificar en caso necesario.

Herencia basada en prototipos

Se basa en que todo tipo/clase tiene un prototipo, el prototipo es el encargado de tener las funciones y atributos comunes. De esta forma las instancias no tienen que tener todo el código duplicado. El referente es JavaScript, pero hay que decir que debido a su naturaleza no está libre de problemas. Cada navegador hace lo que le parece bien, e implementa lo que prefiere. Gracias a basarse en prototipos existen los polyfills. Su objetivo es rellenar las funciones que falten de forma que se puedan utilizar en cualquier navegador.

Composición sobre herencia

No todo en la herencia es tan bonita. Si tenemos una estructura de 10 clases derivadas, si cambiamos el padre habrá que cambiar todas las demás clases. Por eso el gran problema, la herencia podría ser limitante. Aunque componer tampoco es la mejor solución al tener limitación.

Para ello hay soluciones que tienen lo mejor de ambos mundos:

  • Interfaces con métodos por defecto (Java, C#)
  • Incorporar clases dentro de otras (Go)
  • Delegación de métodos en la composición (Rust)

Conclusiones

La herencia puede ser muy potente, pero tiene sus problemas. Para códigos pequeños nos da igual cómo utilizarla, mientras que sea correcto su uso. En códigos más grandes hay que analizar las posibles soluciones en función de lo que sea necesario. Si queremos forzar los cambios utilizamos herencia, y si queremos ampliar el código sin tener que modificar todas las clases lo mejor es la composición.

Fuentes