1.5. Herencia: reutilización de interfaces

En sí misma, la idea de objeto es una herramienta útil. Permite empaquetar datos y funcionalidad junto al propio concepto, además puede representar una idea apropiada del espacio del problema en vez de estar forzado a usar el vocabulario 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 puede 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 manipular más mensajes (o hacerlo de forma diferente). La herencia lo expresa de forma similar 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. Cree un tipo base para representar lo esencial de sus ideas sobre algunos objetos en su sistema. A partir del tipo base, derive otros tipos para expresar caminos diferentes que puede realizar esa parte común.

Por ejemplo, una máquina de reciclado de basura clasifica piezas de basura. El tipo base es «basura», y cada pieza de basura tiene un peso, un valor, y también, 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 (el aluminio puede ser aplastado, el acero puede ser magnético). Además, algunos comportamientos pueden ser diferentes (el valor del papel depende del tipo y condición). 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 juegos 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, cada uno de ellos puede tener características y comportamientos adicionales. Ciertas figuras pueden ser, por ejemplo, rotadas. 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 Figura

Figura 1.4. Jerarquía de Figura


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, lleva directamente desde 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 la 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 datos privados 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 uno de las claves fundamentales para comprender la programación orientada a objetos.

Por lo 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 hace todo lo que necesitamos, por lo que se añaden más funciones. Este uso simple y primitivo de la herencia es, a veces, la solución perfecta a muchos problemas. Sin embargo, quizá debería pensar en la posibilidad de que su clase base puede necesitar también 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 Figura

Figura 1.5. Especialización de Figura


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 respecto de una función de una clase base existente. A esto se le llama reescribir (override) una función.

Reescritura de métodos

Figura 1.6. Reescritura de métodos


Para reescribir una función, simplemente hay que crear una nueva definición para esa función en la clase derivada. Está diciendo, «Estoy usando la misma función de interfaz aquí, pero quiero hacer 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 anular sólo funciones de la clase base (y no añadir nuevos métodos que no estén en la clase base)? Esto puede significar 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 un objeto de una clase derivada por un objeto de la clase base. Se puede pensar como una sustitución pura, y se suele llamar principio de sustitución. En cierto modo, esta es la forma ideal de tratar la herencia. A menudo nos referimos a las relaciones entre la clase base y clases derivadas en este caso como una relación es-un, porque se dice «un círculo es una figura». Un modo de probar la herencia es determinar si se puede considerar la relación es-un sobre las clases y si tiene 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 ser sustituido por el 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 se puede decir que es exactamente el mismo. Por ejemplo, considere un aire acondicionado. Suponga que su casa está conectada con todos los controles para refrigerar; es decir, tiene una interfaz que le permite controlar la temperatura. 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á rentringida 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 bastante general, y se debería renombrar a «sistema de control de temperatura», además también puede incluir calor, en este punto se aplica 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 entender cómo este enfoque (sustitución pura) es la única forma de hacer las cosas, y de hecho es bueno para 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. Con experiencia, ambos casos puede ser razonablemente obvios.