Ahora surge un nuevo problema. Se tiene un IntStack, que maneja enteros. Pero se quiere una pila que maneje formas, o 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 fuente 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++: las plantillas.
La solución de C. Por supuesto, se intenta huir de la aproximación de C porque es desordenada, propensa a errores y nada elegante. En esta aproximación, se copia el código fuente de una Stack y se hacen modificaciones a mano, introduciendo nuevos errores en el proceso. Ciertamente, ésta 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 utilícese 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 como la librería de Smalltalk tiene una importancia tan fundamental, nunca se crea una clase desde cero. 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 por qué se pasa tanto tiempo aprendiendo la librería de clases antes de llegar a 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 está ahí (es la librería de clases de Smalltalk), y la raíz del árbol es una clase llamada Object, la misma clase que manejan los contenedores de Smalltalk.
Es un truco ingenioso porque significa que cada clase en la jerarquía de herencia de Smalltalk (y Java[84]) 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) se conoce como «jerarquía basada en objetos». Se puede haber oído este término y asumido que es un nuevo concepto fundamental de la POO, como el polimorfismo. Sin embargo, simplemente se refiere a una jerarquía de clases con Object (o algún nombre similar) en su raíz y a clases contenedoras que almacenan Objects.
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 implementaciones de C++[85], y como representaba una cantidad importante de código, mucha gente empezó a usarla. 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 magnífica jerarquía basada en objetos con sus clases contenedoras, pero entonces se compra un conjunto de clases de figuras, o de aviones, a otro vendedor que no usa esa jerarquía. (Por un lado, usar esa jerarquía supone una sobrecarga, que los programadores de C rehúyen). ¿Cómo se inserta un árbol de clases independiente en la clase contenedora de la jerarquía basada en objetos? El problema se parece a lo siguiente:
Debido a que C++ admite múltiples jerarquías independientes, la jerarquía basada en objetos de Smalltalk no funciona tan bien.
La solución parecía 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 resolverá el problema. Así que se hace lo siguiente (un ejemplo similar se dio 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 Container. La herencia extra de OCircle, OSquare, etc. es necesaria para que esas clases puedan hacer upcast hacia OShape y mantener así el comportamiento correcto. Se puede ver cómo las cosas se están volviendo confusas rápidamente.
Los vendedores de compiladores inventaron e incluyeron sus propias jerarquías de clases contenedoras basadas en objetos, la mayoría de las cuales han sido reemplazadas desde entonces por versiones con plantillas. 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 sencilla, resulta engorrosa de usar. En su libro original[86], Stroustrup demostró lo que él consideraba una alternativa preferible a la jerarquía basada en objetos. Las clases contenedoras se creaban como grandes macros del preprocesador con argumentos que podían sustituirse por el tipo deseado. Cuando se quería crear un contenedor que manejase un tipo en concreto, se hacían un par de llamadas a macros.
Desafortunadamente, esta aproximación quedó oscurecida por toda la literatura y la experiencia de programación existentes sobre Smalltalk, 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 original basada en 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 [87] (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, una plantilla reutiliza código fuente. El contenedor ya no maneja una clase base genérica llamada Object, sino que gestiona un parámetro no especificado. Cuando se usa una plantilla, 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 toma la versión en plantilla del contenedor y se genera una versión específica para el problema concreto, de este modo:
El compilador hace el trabajo por nosotros, y se obtiene exactamente el contenedor necesario para hacer el trabajo, en vez de una jerarquía de herencia inmanejable. En C++, la plantilla 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 desde el principio clases contenedoras ya hechas (como se ha estado haciendo a lo largo del libro con el vector).
[84] Con la excepción, en Java, de los tipos de datos primitivos, que no se hicieron Objects por eficiencia.
[85] La librería OOPS, por Keith Gorlen, mientras estaba en el NIH.
[86] The C++ Programming Language de Bjarne Stroustrup (1ª edición, Addison-Wesley, 1986).
[87] La inspiración de las plantillas parece venir de los generics de ADA.