4.11 Clases
"One thing I like about C++ is that, unlike Java, C#, and VB.NET, it recognizes no fundamental difference between primitive types and classes. Classes, in C++, are simply types that you create yourself, in effect extending the language". Brian Overland en "Choosing Between References and Pointers in C++" www.informit.com.
§1 Introducción:
Las clases pueden introducirse de muchas formas, comenzando por la que dice que representan un intento de abstraer el mundo real. Pero desde el punto de vista del programador clásico, lo mejor es considerarlas como "entes" que superceden las estructuras C en el sentido de que, tanto los datos, como los instrumentos para su manipulación (funciones), se encuentran encapsulados en ellos. La idea es empaquetar juntos los datos y la funcionalidad; de ahí que tengan dos tipos de componentes (aquí se prefiere llamarlos miembros). Por un lado las propiedades, también llamadas variables, campos ("fields") o atributos, y de otro los métodos, también llamados procedimientos o funciones [1]; más formalmente: variables de clase y métodos de clase.
Nota: la terminología utilizada en la Programación Orientada a Objetos POO (OOP en inglés), no es demasiado consistente, y a veces induce a cierto error a los programadores que se acercan por primera vez con una cultura de programación procedural. De hecho, estas cuestiones semánticas suponen una dificultad adicional en el proceso de entender los conceptos subyacentes en la POO, sus ventajas y su potencial como herramienta.
Las clases C++ ofrecen la posibilidad de extender los tipos predefinidos en el lenguaje (básicos y derivados
2.2).
Cada clase representa un nuevo
tipo; un nuevo conjunto de objetos caracterizado por ciertos valores
(propiedades) y las operaciones (métodos) disponibles para crearlos,
manipularlos y destruirlos [2]. Más tarde se podrán declarar (instanciar) objetos pertenecientes a
dicho tipo (clase) del mismo modo que se hace para las variables simples tradicionales.
Nota: considerando que son vehículos para manejo y manipulación de información, las clases han sido comparadas en ocasiones con los sistemas tradicionales de manejo de datos DBMS ("DataBase Management System"); aunque de un tipo muy especial, ya que sus características les permiten operaciones que están absolutamente prohibidas a los sistemas DBMS clásicos [5].
La mejor manera de entender las clases es considerar que se trata simplemente
de tipos de datos cuya única peculiaridad es que pueden ser definidos por el
usuario. Generalmente se trata de tipos complejos, constituidos a su vez
por elementos de cualquier tipo (incluso otras clases). La definición
que puede hacerse de ellos no se reduce a diseñar su "contenido"; también pueden definirse su álgebra y su
interfaz. Es decir: como se opera con estos tipos y como los ve el usuario
(qué puede hacer con ellos).
El inventor del lenguaje señala que la principal razón para definir
un nuevo tipo es separar los detalles poco relevantes de la implementación de
las propiedades que son verdaderamente esenciales para utilizarlos correctamente [6].
§2 En este sentido, podríamos establecer una analogía entre un tipo de dato simple, por ejemplo un
int, y una clase a la que llamaremos CL:
§2.1 Ambos son "tipos"
a1.- El tipo int está preconstruido en el lenguaje; cuando nos referimos a un int, está completamente determinado que significa y que operaciones pueden realizarse con los objetos de su "clase" (los enteros).
a2.- El tipo CL no está preconstruido (no existe previamente en el lenguaje) por lo que antes de usado debe ser definido. La definición de la clase es incumbencia del programador, en ella se determina qué operaciones se pueden realizar con este tipo de objetos. La definición de una clase puede tener el aspecto siguiente:
class CL { /* definición de la clase */ };
§2.2 Pueden declararse objetos de ese tipo:
b1.- Puede declararse un objeto x como perteneciente al tipo int: (diríamos que x es un entero):
int x; // declara x perteneciente al tipo int
b2.- Puede declararse un objeto c1 como perteneciente al tipo CL. En este caso la POO tiene su propio vocabulario; prefiere decir que se "instancia" la clase (diríamos que c1 es una instancia u objeto):
CL c1; // declara c1 perteneciente al tipo CL
§2.3 Pueden asignarse valores a los objetos:
c1.- Puede asignarse un valor al objeto x:
x = y; // asigna a x el valor de y (que suponemos un objeto tipo int)
c2.- Puede asignarse un valor al objeto c1:
c1 = d1; // asigna a c1 el valor de d1 (que suponemos un objeto tipo CL)
§2.4 Pueden realizarse operaciones con los objetos:
d1.- Pueden realizarse operaciones con objetos tipo int. Por ejemplo, en el caso de los enteros, está perfectamente definido en el lenguaje que resultado produce el operador "+" entre dos objetos de dicho tipo.
x = y + z; // suma aritmética de z e y
d2.- Puede realizarse operaciones con objeto tipo CL (suponiendo que estas operaciones hayan sido definidas por el programador de la clase):
c1 = d1 + e; // el resultado que haya establecido el programador de la clase CL
Al llegar a este punto se terminaría la posible analogía entre datos simples y clases porque estas últimas tienen en
realidad una "doble personalidad". En efecto, hemos señalado
( 2) que en un programa existen dos
tipos de elementos: datos e instrucciones. Los tipos básicos antes aludidos son exclusivamente "datos", y en
la programación clásica la funcionalidad está dispersa en el código del programa (generalmente agrupada en unidades lógicas a
las que denominamos "funciones"). Los lenguajes orientados a objetos como C++ permiten que las clases contengan
también "funcionalidad", además de "datos", de ahí nuestra afirmación de que las clases tienen una
doble personalidad. Esta funcionalidad se concreta en funciones alojadas en su interior. De hecho, gran parte del trabajo
de las clases en los programas se realiza a través de estas funciones-miembro (métodos).
Nota: generalmente parte de esta "funcionalidad" de los métodos se utiliza para manipular los datos, incluyendo accederlos (verlos) al exterior, por lo que algunos autores [7] insisten en que los métodos son en realidad parte de la información (de los datos).
Por ejemplo, no es infrecuente ver en compiladores C++ o de
otros lenguajes de alto nivel orientados a objetos [3],
clases predefinidas de tipo "Browse" que son capaces de manejar el
contenido de una base de datos de forma absolutamente cómoda para el
programador que las usa. Están definidas de tal modo que mediante
invocaciones a sus diversos métodos pueden mostrarnos el contenido de la base
de datos en forma de filas y columnas; permite definir el tamaño de la
ventana o incluso redimensionarla en tiempo de ejecución; desplazarse por sus
celdas mediante la acción de las teclas de movimiento de cursor; ir al
comienzo, al final, o a cualquier registro que deseemos; hacer
"scroll" horizontal y vertical mediante barras de deslizamiento; etc. etc.
En la literatura sobre el tema es frecuente encontrar expresiones como: "enviarle un mensaje al objeto". Se refieren a invocar una de estas funciones-miembro que son accesibles desde el exterior; lo que en lenguaje coloquial significa enviar al objeto una petición de que ponga en marcha alguna de las funcionalidades inherentes a "su clase".
§3 Agrupar objetos y funcionalidad
El hecho de que las clases encapsulen en una misma entidad datos y funcionalidad (su álgebra y su interfaz), supone una ventaja determinante a la hora de escribir y mantener aplicaciones grandes, y es sin duda una de las razones a las que C++ debe su éxito como lenguaje de opción para grandes proyectos.
Los métodos tradicionales de programación exigían que cuando una regla de operación cambiase en un programa (cosa que suele ocurrir con suma frecuencia), el programador debía rastrear todo el código de la aplicación para ir modificando todas las ocurrencias en que apareciese dicha operación, adaptándola a las nuevas circunstancias. Por contra, la POO permite modificar el "álgebra" de la clase sin preocuparse de nada más, ya que toda la operatoria está concentrada en un punto.
Nota: como ejemplo, puedo contaros que hace años tuve que escribir una aplicación relativamente grande para un negocio de distribución de libros. Durante el análisis me enteré que debía almacenar un dato denominado Código ISBN. Un código que acompaña a cada libro y que es único en el mundo (no hay dos libros distintos con el mismo ISBN). El cliente me informó que era un código exclusivamente numérico de una longitud máxima de X cifras. Después de casi un año de trabajo, durante las primeras pruebas, en que pedimos al almacén varios ejemplares para comprobar el proceso de introducción de datos en condiciones reales, apareció un código ISBN que contenía un carácter alfabético. Ni que decir tiene que la consternación fue mayúscula. El cliente puso cara de sorprendido y me juró que le habían asegurado que bla, bla, bla. Desgraciadamente la aplicación no estaba escrita en un lenguaje orientado a objetos (como era normal en la época), así que su utilización sufrió un retraso de varios meses hasta que el nuevo tipo (alfanumérico) fue corregido en todos los ficheros y líneas de código que lo utilizaban. Actualmente, con un lenguaje orientado a objetos como C++, solo hubiera tenido que modificar unas cuantas líneas de código en la clase Libro.
§4 Ocultar los detalles
En realidad las clases C++ son algo más que simple funcionalidad añadida a las estructuras del C clásico. Veremos que además de la posibilidad de contener funciones, las clases C++ disponen de un mecanismo especial de acceso que a fin de cuentas, se traduce en que algunos de sus miembros permanecen ocultos al exterior.
La idea no es solamente encapsular juntos datos y funcionalidad; se trata también de que la clase actúe como un subsistema cerrado dentro del contexto general del programa que la utiliza. De este subsistema solo interesa y es accesible determinada información (datos) y funcionalidad (métodos), sin que importe ni pueda manipularse su interior de ninguna otra forma.
La forma concreta de conseguir este control de acceso es haciendo que no todas las variables y funciones-miembro sean accesibles desde el exterior. De hecho, esto ocurre solo con algunas, las denominadas "públicas", que constituyen la parte visible del objeto (su interfaz). Al mismo tiempo, pueden existir una cantidad de otros miembros (los denominados "privados") que no son visibles. La razón de la existencia de los miembros privados es proporcionar cierta funcionalidad interna para soportar la externa. Vendrían a ser como el cuerpo de una función que invocamos pasándole unos argumentos y que nos devuelve un valor. Solo nos interesa el argumento que hay que "enviarle" y el valor devuelto, no interesa en absoluto lo que ocurre "dentro" de la función o "como" es el detalle de la obtención del resultado.
Nota: en general, se considera como una buena práctica definir las clases de forma que sus propiedades sean siempre privadas, consiguiendo así que no sean directamente accesibles desde el exterior, y que solo puedan ser vistas y manipuladas a través de métodos públicos específicos diseñados al efecto, que constituyen la interfaz, y garantizan que la manipulación y acceso a las propiedades del objeto se realiza dentro de las condiciones exigidas por la aplicación. Por esta razón, tales métodos se denominan accessors o get methods y mutators o set methods (que podríamos traducir por funciones procuradoras y modificadoras). En caso necesario, los métodos públicos se completan con otros métodos auxiliares privados (helper functions) que aportan funcionalidad complementaria a la interfaz.
A primera vista podría parecer que este sistema de ocultación no tiene
demasiado sentido para una clase definida y utilizada por un programador en su
propia aplicación; a fin de cuentas, puesto que la define, siempre
puede modificarla y accederla como le plazca. Sin embargo, si pensamos
que las clases pueden venir empaquetadas en librerías, y que estas librerías
son muchas veces confeccionadas por otras personas distintas del programador
que las usa, el asunto cobra su verdadero sentido. Por ejemplo, no
existe el peligro de que por una manipulación indebida el programador-usuario
estropee o corrompa el mecanismo interno de la clase que otros han
confeccionado y cuyos detalles de funcionamiento interno él desconoce.
Este sistema de protección puede servir incluso para el programador que las diseña y usa en sus propias aplicaciones, puesto que una vez puestas a punto, puede usarlas y rehusarlas (como material-base para construir nuevas clases) sin preocuparse más por su diseño, ya que son subsistemas estables y probados.
Una ventaja adicional no menos importante para los fabricantes de clases (en el mundo de la programación también existe un mercado de objetos prefabricados [3]), es que una vez publicada la especificación de una clase y su interfaz (que hace y como se usa), las actualizaciones posteriores no afectan para nada a los usuarios de versiones antiguas que quieran actualizarse. Solamente hay que procurar que la interfaz se mantenga exactamente igual. En todo caso solo es necesario documentar las nuevas funcionalidades si las hubiere.
Una tercera ventaja es la que podríamos llamar de protección del "know-how" o secreto industrial. Es posible implementar una clase o conjunto de ellas, con una funcionalidad concreta (por ejemplo, una librería de comunicaciones IP) sin necesidad de desvelar todos sus detalles [4], solo es necesario publicar su interfaz. En la mayoría de los casos los autores de librerías comerciales adoptan todas las medidas posibles para evitar que incluso los ficheros de cabecera puedan revelar información estratégica a sus competidores. Por ejemplo, la técnica del "Gato de Cheshire" ( 4.13.4).
[1] Se prefiere método más que función, para distinguirlos de las funciones de la programación tradicional (procedural). Veremos a continuación que algunas de estas funciones reciben nombres especiales. Por ejemplo, constructores, destructores, accessors y mutators.
[2] Este conjunto de operaciones y valores visibles desde el exterior por el "usuario" de la clase (el programador), es lo que se denomina "interfaz" de la clase.
[3] Tradicionalmente los fabricantes de "accesorios" para programación, proporcionaban funciones en forma de librerías .LIB o .OBJ, que enlazadas con nuestro código, permitían invocar dichas funciones para conseguir la "funcionalidad" propuesta. Con la popularización de la POO, estos accesorios vienen suministrados en forma de clases. Por lo general, los fabricantes de compiladores las ofrecen bajo la interfaz de las cajas de herramientas o componentes incluidas en las modernas "suites" RAD ( 1.8). Por ejemplo, los entornos de desarrollo C++Builder o Visual C++ incluyen un potente conjunto de ellas. Además, existe un extenso mercado de "componentes" C++ listos para usar con las funcionalidades más variadas. Son lo que en el mundillo de la programación se denominan componentes de terceras partes (3pp), en referencia a que no son las incluidas de forma estándar con los compiladores ni creadas por el programador de la aplicación.
[4] Salvo la hipótesis de efectuar ingeniería inversa sobre ellas, las librerías son una caja negra desde el exterior. Además, salvo excepciones, el esfuerzo necesario para desensamblar completamente la ingeniería de una librería es superior al esfuerzo de construirla uno mismo.
[5] Al Stevens "Persistent objects in C++" Dr. Dobb's Journal. Dic 1992.