1.5. Herencia: reutilización de interfaces

En sí misma, la idea de objeto es una herramienta útil. Permite agrupar datos y funcionalidad por conceptos, de modo que puede representar una idea apropiada del espacio del problema en vez de verse forzado a usar los modismos de la máquina subyacente. Esos conceptos se expresan como unidades fundamentales en el lenguaje de programación mediante la palabra reservada class.

Sin embargo, es una pena tomarse tantas molestias en crear una clase y verse obligado a crear una más para un nuevo tipo que tiene una funcionalidad similar. Es más sencillo si se puede usar la clase existente, clonarla, y hacerle añadidos y modificaciones a ese clon. Esto es justamente lo que hace la herencia, con la excepción de que si cambia la clase original (llamada clase base, super o padre), el «clon» modificado (llamado clase derivada, heredada, sub o hija) también refleja esos cambios.

subclases

Figura 1.3. subclases


(En el diagrama UML anterior, la flecha apunta desde la clase derivada hacia la clase base. Como verá, puede haber más de una clase derivada.)

Un tipo hace algo más que describir las restricciones de un conjunto de objetos; también tiene una relación con otros tipos. Dos tipos pueden tener características y comportamientos en común, pero un tipo puede contener más características que otro y también puede atender más mensajes (o hacerlo de forma diferente). La herencia expresa esta similitud entre tipos usando el concepto de tipos base y tipos derivados. Un tipo base contiene todas las características y comportamientos compartidos entre los tipos derivados de él. Se crea un tipo base para representar lo esencial de sus ideas sobre algunos objetos de su sistema. A partir del tipo base, se derivan otros tipos para expresar las diferentes formas en que se puede materializar esa parte esencial.

Por ejemplo, una máquina de reciclado de basura clasifica trozos de basura. El tipo base es «basura», y cada trozo de basura tiene un peso, un valor, etc., y se puede triturar, fundir o descomponer. A partir de ahí, se obtienen más tipos específicos de basura que pueden tener características adicionales (una botella tiene un color) o comportamientos (una lata de aluminio se puede aplastar, una lata de acero es magnética). Además, algunos comportamientos pueden ser diferentes (el valor del papel depende de su tipo y de su estado). Usando la herencia, se puede construir una jerarquía de tipos que exprese el problema que se intenta resolver en términos de sus tipos.

Un segundo ejemplo es el clásico ejemplo «figura», tal vez usado en un sistema de diseño asistido por computador o en un juego de simulación. El tipo base es figura, y cada figura tiene un tamaño, un color, una posición y así sucesivamente. Cada figura se puede dibujar, borrar, mover, colorear, etc. A partir de ahí, los tipos específicos de figuras derivan (heredan) de ella: círculo, cuadrado, triángulo, y así sucesivamente, y cada uno de ellos puede tener características y comportamientos adicionales. Ciertas figuras pueden ser, por ejemplo, volteadas. Algunos comportamientos pueden ser diferentes, como cuando se quiere calcular el área de una figura. La jerarquía de tipos expresa las similitudes y las diferencias entre las figuras.

Jerarquía de Shape

Figura 1.4. Jerarquía de Shape


Modelar la solución en los mismos términos que el problema es tremendamente beneficioso porque no se necesitan un montón de modelos intermedios para transformar una descripción del problema en una descripción de la solución. Con objetos, la jerarquía de tipos es el principal modelo, así que se pasa directamente de la descripción del sistema en el mundo real a la descripción del sistema en código. Efectivamente, una de las dificultades que la gente tiene con el diseño orientado a objetos es que es demasiado fácil ir desde el principio hasta el final. Una mente entrenada para buscar soluciones complejas a menudo se confunde al principio a causa de esta simplicidad.

Cuando se hereda de un tipo existente, se está creando un tipo nuevo. Este nuevo tipo contiene no sólo todos los miembros del tipo base (aunque los miembros private están ocultos e inaccesibles), sino que además, y lo que es más importante, duplica la interfaz de la clase base. Es decir, todos los mensajes que se pueden enviar a los objetos de la clase base se pueden enviar también a los objetos de la clase derivada. Dado que se conoce el tipo de una clase por los mensajes que se le pueden enviar, eso significa que la clase derivada es del mismo tipo que la clase base. En el ejemplo anterior, «un círculo es una figura». Esta equivalencia de tipos vía herencia es una de las claves fundamentales para comprender la programación orientada a objetos.

Dado que tanto la clase base como la derivada tienen la misma interfaz, debe haber alguna implementación que corresponda a esa interfaz. Es decir, debe haber código para ejecutar cuando un objeto recibe un mensaje particular. Si simplemente hereda de una clase y no hace nada más, los métodos de la interfaz de la clase base están disponibles en la clase derivada. Esto significa que los objetos de la clase derivada no sólo tienen el mismo tipo, también tienen el mismo comportamiento, lo cual no es particularmente interesante.

Hay dos caminos para diferenciar la nueva clase derivada de la clase base original. El primero es bastante sencillo: simplemente hay que añadir nuevas funciones a la clase derivada. Estas nuevas funciones no son parte de la interfaz de la clase base. Eso significa que la clase base simplemente no hacía todo lo que usted quería, por lo que ha añadido más funciones. Este uso simple y primitivo de la herencia es, a veces, la solución perfecta a su problema. Sin embargo, debería considerar detenidamente la posibilidad de que su clase base pueda necesitar también esas funciones adicionales. Este proceso de descubrimiento e iteración de su diseño ocurre regularmente en la programación orientada a objetos.

Especialización de Shape

Figura 1.5. Especialización de Shape


Aunque la herencia algunas veces supone que se van a añadir nuevas funciones a la interfaz, no es necesariamente cierto. El segundo y más importante camino para diferenciar su nueva clase es cambiar el comportamiento de una función de una clase base existente. A esto se le llama redefinir (override) una función.

Redefinición de métodos

Figura 1.6. Redefinición de métodos


Para redefinir una función, simplemente hay que crear una nueva definición para esa función en la clase derivada. Es como decir: «Estoy usando la misma función de interfaz aquí, pero quiero que haga algo diferente para mi nuevo tipo».

1.5.1. Relaciones es-un vs. es-como-un

Hay cierta controversia que puede ocurrir con la herencia: ¿la herencia debería limitarse a redefinir sólo funciones de la clase base (y no añadir nuevos métodos que no estén en la clase base)? Esto significaría que el tipo derivado es exactamente el mismo tipo que la clase base dado que tiene exactamente la misma interfaz. Como resultado, se puede sustituir exactamente un objeto de la clase base por un objeto de la clase derivada. Se puede pensar como una sustitución pura, y se suele llamar principio de sustitución. En cierto modo, ésta es la forma ideal de tratar la herencia. A menudo se dice que, en este caso, la relación entre la clase base y las clases derivadas es una relación es-un, porque se puede decir «un círculo es una figura». Una forma de comprobar si procede la herencia es determinar si se puede enunciar la relación es-un entre las clases y que tenga sentido.

Hay ocasiones en las que se deben añadir nuevos elementos a la interfaz de un tipo derivado, de esta manera se amplía la interfaz y se crea un tipo nuevo. El nuevo tipo todavía puede sustituir al tipo base, pero la sustitución no es perfecta porque sus nuevas funciones no son accesibles desde el tipo base. Esta relación se conoce como es-como-un; el nuevo tipo tiene la interfaz del viejo tipo, pero también contiene otras funciones, por lo que no se puede decir que sea exactamente el mismo. Por ejemplo, considere un aire acondicionado. Suponga que su casa tiene instalados todos los controles para la refrigeración; es decir, tiene una interfaz que le permite controlar la refrigeración. Imagine que el aire acondicionado se avería y lo reemplaza por una bomba de calor, la cual puede dar calor y frío. La bomba de calor es-como-un aire acondicionado, pero puede hacer más cosas. Como el sistema de control de su casa está diseñado sólo para controlar el frío, está restringido a comunicarse sólo con la parte de frío del nuevo objeto. La interfaz del nuevo objeto se ha extendido, y el sistema existente no conoce nada excepto la interfaz original.

Relaciones

Figura 1.7. Relaciones


Por supuesto, una vez que vea este diseño queda claro que la clase base «sistema de frío» no es lo bastante general, y se debería renombrar a «sistema de control de temperatura» para que también pueda incluir la calefacción, y en ese momento funcionará el principio de sustitución. Sin embargo, el diagrama de arriba es un ejemplo de lo que puede ocurrir en el diseño y en el mundo real.

Cuando se ve el principio de sustitución es fácil sentir que este enfoque (sustitución pura) es la única forma de hacer las cosas, y de hecho es bueno que sus diseños funcionen de esta forma. Pero verá que hay ocasiones en que está igualmente claro que se deben añadir nuevas funciones a la interfaz de la clase derivada. Examinándolos, ambos casos deberían resultar razonablemente obvios.