Fundamentos de la programación orientada a objetos: herencia

Cuando se construyen proyectos grandes y complejos, la gente busca formas de hacer más eficiente el proceso. Una de las formas más populares es aprovechar los desarrollos existentes. Por ejemplo, es mucho más fácil desarrollar un plan de construcción no desde cero, sino basándose en los planos anteriores.

También en programación es muy popular la reutilización de código. Ya conocemos una de estas técnicas: aislar un fragmento de código en una función y luego llamarla desde distintos lugares en los que se requiera la funcionalidad correspondiente. Pero la programación orientada a objetos ofrece un mecanismo más potente: al desarrollar una nueva clase, ésta puede heredar de otra, adquiriendo toda la estructura interna y la interfaz externa, lo que requiere sólo un ajuste mínimo para adaptarse al propósito. Así, partiendo de la clase padre, se puede «hacer crecer» rápidamente una clase derivada con capacidades adicionales o afinadas. Además, cualquier cambio posterior en la clase padre (como mejoras o correcciones de errores) afectará automáticamente a todas las clases hijas.

Cuando una clase es padre de otra, se denomina clase base. A su vez, la clase que hereda de la clase base se denomina derivada.

Por supuesto, la cadena de herencia (o, mejor dicho, el árbol genealógico) puede continuar: cada clase puede tener varios herederos y éstos, a su vez, sus propios herederos, y así sucesivamente. Lo único que no permiten las reglas de herencia son los ciclos en las relaciones de parentesco; por ejemplo, un nieto no puede ser padre de su abuelo.

La relación entre cualquier clase y su descendiente de cualquier generación se describe mediante las palabras «es un(a)», es decir, el descendiente puede actuar como antepasado, pero no viceversa. Esto se debe a que el objeto derivado contiene en realidad el modelo de datos del ancestro y lo complementa con nuevos campos y comportamientos.

Al heredar clases entre sí, tenemos la oportunidad de procesar objetos relacionados de forma unificada, ya que algunas de sus funciones son comunes.

Por ejemplo, un hipotético programa de dibujo puede utilizarse para implementar varios tipos de formas, como círculos, cuadrados, triángulos, etc. Cada objeto tiene coordenadas en la pantalla (para simplificar, supondremos que se especifica un par de valores X e Y del centro de la forma). Además, cada forma se representa con su propio color de fondo, color de borde y grosor de borde.

Esto significa que podemos implementar funciones para establecer las coordenadas y el estilo de dibujo sólo una vez en la clase padre que describe la forma abstracta, y estas funciones serán heredadas automáticamente por todos los descendientes.

Además, para simplificar el código fuente, es conveniente unificar de algún modo no sólo los ajustes, sino también el dibujo de las distintas formas. Esta frase encierra algún tipo de contradicción: puesto que las formas son diferentes y cada una debe mostrarse a su manera, ¿de qué tipo de unificación estamos hablando? Hablamos de una interfaz de software unificada. De hecho, según el concepto de abstracción, es necesario separar la interfaz externa de la implementación interna. Y la visualización de formas específicas es en esencia un detalle de implementación.

Una interfaz unificada y diferentes implementaciones para los tipos de forma nos llevan fácilmente al siguiente concepto: el polimorfismo.