Ahora surge un nuevo problema. Tenemos un
IntStack
, que maneja enteros. Pero
queremos una pila que maneje formas, o flotas de aviones, o
plantas o cualquier otra cosa. Reinventar el código fuente cada
vez no parece una aproximación muy inteligente con un lenguaje
que propugna la reutilización. Debe haber un camino mejor.
Hay tres técnicas para reutilizar código en esta situación: el modo de C, presentado aquí como contraste; la aproximación de Smalltalk, que afectó de forma significativa a C++, y la aproximación de C++: los templates.
La solución de C. Por supuesto hay que
escapar de la aproximación de C porque es desordenada y provoca
errores, al mismo tiempo que no es nada elegante. En esta
aproximación, se copia el código de una Stack
y se hacen modificaciones a mano, introduciendo
nuevos errores en el proceso. Esta no es una técnica muy
productiva.
La solución de Smalltalk. Smalltalk (y Java
siguiendo su ejemplo) optó por una solución simple y directa: Se
quiere reutilizar código, pues utilicese la herencia. Para
implementarlo, cada clase contenedora maneja elementos de una
clase base genérica llamada Object
(similar
al ejemplo del final del capítulo 15). Pero debido a que la
librería de Smalltalk es fundamental, no se puede crear una
clase desde la nada. En su lugar, siempre hay que heredar de una
clase existente. Se encuentra una clase lo más cercana posible a
lo que se desea, se hereda de ella, y se hacen un par de
cambios. Obviamente, esto es un beneficio porque minimiza el
trabajo (y explica porque se pierde un montón de tiempo
aprendiendo la librería antes de ser un programador efectivo en
Smalltalk).
Pero también significa que todas las clases de Smalltalk acaban
siendo parte de un único árbol de herencia. Hay que heredar de
una rama de este árbol cuando se está creando una nueva
clase. La mayoría del árbol ya esta allí (es la librería de
clases de Smalltalk), y la raiz del árbol es una clase llamada
Object
- la misma clase que los contenedores
de Smalltalk manejan.
Es un truco ingenioso porque significa que cada clase en la
jerarquía de herencia de Smalltalk (y Java[80]) se deriva de Object
, por lo
que cualquier clase puede ser almacenada en cualquier contenedor
(incluyendo a los propios contenedores). Este tipo de jerarquía
de árbol única basada en un tipo genérico fundamental (a menudo
llamado Object
, como también es el caso
en Java) es conocido como "jerarquía basada en objectos". Se
puede haber oido este témino y asumido que es un nuevo concepto
fundamental de la POO, como el polimorfismo. Sin embargo,
simplemente se refiere a la raíz de la jerarquía como
Object
(o algún témino similar) y a
contenedores que almacenan Object
s.
Debido a que la librería de clases de Smalltalk tenía mucha más experiencia e historia detrás de la que tenía C++, y porque los compiladores de C++ originales no tenían librerías de clases contenedoras, parecía una buena idea duplicar la librería de Smalltalk en C++. Esto se hizo como experimento con una de las primeras implementaciónes de C++[81], y como representaba un significativo ahorro de código mucha gente empezo a usarlo. En el proceso de intentar usar las clases contenedoras, descubrieron un problema.
El problema es que en Smalltalk (y en la mayoría de los lenguajes de POO que yo conozco), todas las clases derivan automáticamente de la jerarquía única, pero esto no es cierto en C++. Se puede tener una magnifica jerarquía basada en objetos con sus clases contenedoras, pero entonces se compra un conjunto de clases de figuras, o de aviones de otro vendedor que no usa esa jerarquía. (Esto se debe a que usar una jerarquía supone sobrecarga, rechazada por los programadores de C). ¿Cómo se inserta un árbol de clases independientes en nuestra jerarquía? El problema se parece a lo siguiente:
Debido a que C++ suporta múltiples jerarquías independientes, la jerarquía basada en objetos de Smalltalk no funciona tan bien.
La solución parace obvia. Si se pueden tener múltiples jerarquías de herencia, entonces hay que ser capaces de heredar de más de una clase: La herencia múltiple resuelve el problema. Por lo que se puede hacer lo siguiente (un ejemplo similar se dió al final del Capítulo 15).
Ahora OShape
tiene las características y
el comportamiento de Shape
, pero como
también está derivado de Object
puede ser
insertado en el contenedor. La herencia extra dada a
OCircle
, OSquare
,
etc. es necesaria para que esas clases puedan hacer upcast hacia
OShape
y puedan mantener el
comportamiento correcto. Se puede ver como las cosas se están
volviendo confusas rápidamente.
Los vendedores de compiladores inventaron e incluyeron sus propias jerarquías y clases contenedoras, muchas de las cuales han sido reemplazadas desde entonces por versiones de templates. Se puede argumentar que la herencia múltiple es necesaria para resolver problemas de programación general, pero como se verá en el Volumen 2 de este libro es mejor evitar esta complejidad excepto en casos especiales.
Aunque una jerarquía basada en objetos con herencia múltiple es conceptualmente correcta, se vuelve difícil de usar. En su libro[82], Stroustrup demostró lo que el consideraba una alternativa preferible a la jerarquía basada en objetos. Clases contenedoras que fueran creadas como grandes macros del preprocesador con argumentos que pudieran ser sustituidos con el tipo deseado. Cuando se quiera crear un contenedor que maneje un tipo en concreto, se hacen un par de llamadas a macros.
Desafortunadamente, esta aproximación era confusa para toda la literatura existente de Smalltalk y para la experiencia de programación, y era un poco inmanejable. Básicamente, nadie la entendía.
Mientras tanto, Stroustrup y el equipo de C++ de los
Laboratorios Bell habían modificado su aproximación de las
macros, simplificándola y moviéndola del dominio del
preprocesador al compilador. Este nuevo dispositivo de
sustitución de código se conoce como template
[83]
(plantilla), y representa un modo completamente diferente de
reutilizar el código. En vez de reutilizar código objeto, como
en la herencia y en la composición, un template reutiliza
código fuente. El contenedor no maneja una
clase base genérica llamada Object
, si no
que gestiona un parámetro no especificado. Cuando se usa un
template, el parámetro es sustituido por el
compilador, parecido a la antigua aproximación de las
macros, pero más claro y fácil de usar.
Ahora, en vez de preocuparse por la herencia o la composición cuando se quiera usar una clase contenedora, se usa la versión en plantilla del contenedor y se crea una versión específica para el problema, como lo siguiente:
El compilador hace el trabajo por nosotros, y se obtiene el
contenedor necesario para hacer el trabajo, en vez de una
jerarquía de herencia inmanejable. En C++, el template
implementa el concepto de tipo
parametrizado. Otro beneficio de la aproximación de
las plantillas es que el programador novato que no tenga
familiaridad o esté incómodo con la herencia puede usar las
clases contenedoras de manera adecuada (como se ha estado
haciendo a lo largo del libro con el
vector
).
[80] Con
la excepción, en Java, de los tipos de datos primitivos, que se
hicieron no Object
s por eficiencia.
[81] La librería OOPS, por Keith Gorlen, mientras estaba en el NIH.
[82] The C++ Programming Language by Bjarne Stroustrup (1ª edición, Addison-Wesley, 1986)
[83]
La inspiración de los templates parece venir de los
generics
de ADA