6.4. ¿Cómo se diseñan las clases?
En estos primeros ejemplos, hemos "pensado" qué objetos necesitaríamos, y hemos empezado a teclear directamente para implementarlos. Esto no es lo habitual. Normalmente, se usan herramientas gráficas que nos ayuden a visualizar las clases y las relaciones que existen entre ellas. También se puede dibujar directamente en papel para aclararnos las ideas, pero el empleo de herramientas informáticas tiene una ventaja adicional: algunas de ellas nos permiten generar automáticamente un esqueleto del programa.
La metodología más extendida actualmente para diseñar estos objetos y sus interacciones (además de otras muchas cosas) se conoce como UML (Unified Modelling Language, lenguaje de modelado unificado). El estándar UML propone distintos tipos de diagramas para representar los posibles "casos de uso" de una aplicación, la secuencia de acciones que se debe seguir, las clases que la van a integrar (es lo que a nosotros nos interesa en este momento), etc.
Vamos a ver la apariencia que tendría un "diagrama de clases". En concreto, vamos a ver un ejemplo usando ArgoUML, que es una herramienta gratuita de modelado UML, que está creada en Java, por lo que se puede utilizar desde multitud de sistemas operativos.
Ampliando el ejemplo anterior, vamos a suponer que queremos hacer un sistema "domótico", para automatizar ciertas funciones en una casa: apertura y cierre de ventanas y puertas, encendido de calefacción, etc.
Las ideas iniciales de las que partiremos son:
- La casa es el conjunto ("agregación") de varios elementos: puertas, ventanas y calefactores.
- Cada puerta se puede abrir y cerrar.
- Cada ventana se puede abrir, cerrar. Además, las ventanas tienen persianas, que se pueden subir y bajar.
- Cada calefactor puede encenderse, apagarse o se puede programar para que trabaje a una cierta temperatura.
Con estas posibilidades básicas, el diagrama de clases podría ser así:
Este diagrama es una forma más simple de ver las clases existentes y las relaciones entre ellas. Si generamos las clases a partir del diagrama, tendremos parte del trabajo hecho: ya "sólo" nos quedará rellenar los detalles de métodos como "Abrir", pero el esqueleto de todas las clases ya estará "escrito" para nosotros.
6.5. La palabra "static"
Desde un principio, nos hemos encontrado con que "Main" siempre iba acompañado de la palabra "static". En cambio, los métodos (funciones) que pertenecen a nuestros objetos no los estamos declarando como "static". Vamos a ver el motivo:
La palabra "static" delante de un atributo (una variable) de una clase, indica que es una "variable de clase", es decir, que su valor es el mismo para todos los objetos de la clase. Por ejemplo, si hablamos de coches convencionales, podríamos suponer que el atributo "numeroDeRuedas" va a valer 4 para cualquier objeto que pertenezca a esa clase (cualquier coches). Por eso, se podría declarar como "static".
De igual modo, si un método (una función) está precedido por la palabra "static", indica que es un "método de clase", es decir, un método que se podría usar sin necesidad de declarar ningún objeto de la clase. Por ejemplo, si queremos que se pueda usar la función "BorrarPantalla" de una clase "Hardware" sin necesidad de crear primero un objeto perteneciente a esa clase, lo podríamos conseguir así:
public class Hardware { ... public static void BorrarPantalla () { ... }
que desde dentro de "Main" (incluso perteneciente a otra clase) se usaría con el nombre de la clase delante:
public class Juego { ... public ComienzoPartida() { Hardware.BorrarPantalla (); ...
Desde una función "static" no se puede llamar a otras funciones que no lo sean. Por eso, como nuestro "Main" debe ser static, deberemos siempre elegir entre:
- Que todas las demás funciones de nuestro fuente también estén declaradas como "static", por lo que podrán ser utilizadas desde "Main".
- Declarar un objeto de la clase correspondiente, y entonces sí podremos acceder a sus métodos desde "Main":
public class Ejemplo { ... public LanzarJuego () { Juego j = new Juego(); j.ComienzoPartida (); ...
6.6. Constructores y destructores.
Hemos visto que al declarar una clase, se dan valores por defecto para los atributos. Por ejemplo, para un número entero, se le da el valor 0. Pero puede ocurrir que nosotros deseemos dar valores iniciales que no sean cero. Esto se puede conseguir declarando un "constructor" para la clase.
Un constructor es una función especial, que se pone en marcha cuando se crea un objeto de una clase, y se suele usar para dar esos valores iniciales, para reservar memoria si fuera necesario, etc.
Se declara usando el mismo nombre que el de la clase, y sin ningún tipo de retorno. Por ejemplo, un "constructor" para la clase Puerta que le diera los valores iniciales de 100 para el ancho, 200 para el alto, etc., podría ser así:
public Puerta() { ancho = 100; alto = 200; color = 0xFFFFFF; abierta = false; }
Podemos tener más de un constructor, cada uno con distintos parámetros. Por ejemplo, puede haber otro constructor que nos permita indicar el ancho y el alto:
public Puerta(int an, int al) { ancho = an; alto = al; color = 0xFFFFFF; abierta = false; }
Ahora, si declaramos un objeto de la clase puerta con "Puerta p = new Puerta();" tendrá de ancho 100 y de alto 200, mientras que si lo declaramos con "Puerta p2 = new Puerta(90,220);" tendrá 90 como ancho y 220 como alto.
Un programa de ejemplo que usara estos dos constructores para crear dos puertas con características iniciales distintas podría ser:
/*---------------------------*/ /* Ejemplo en C# nº 61: */ /* ejemplo61.cs */ /* */ /* Tercer ejemplo de clases */ /* Constructores */ /* */ /* Introduccion a C#, */ /* Nacho Cabanes */ /*---------------------------*/ using System; public class Puerta { int ancho; // Ancho en centimetros int alto; // Alto en centimetros int color; // Color en formato RGB bool abierta; // Abierta o cerrada public Puerta() { ancho = 100; alto = 200; color = 0xFFFFFF; abierta = false; } public Puerta(int an, int al) { ancho = an; alto = al; color = 0xFFFFFF; abierta = false; } public void Abrir() { abierta = true; } public void Cerrar() { abierta = false; } public void MostrarEstado() { Console.WriteLine("Ancho: {0}", ancho); Console.WriteLine("Alto: {0}", alto); Console.WriteLine("Color: {0}", color); Console.WriteLine("Abierta: {0}", abierta); } } // Final de la clase Puerta public class Ejemplo61 { public static void Main() { Puerta p = new Puerta(); Puerta p2 = new Puerta(90,220); Console.WriteLine("Valores iniciales..."); p.MostrarEstado(); Console.WriteLine("\nVamos a abrir..."); p.Abrir(); p.MostrarEstado(); Console.WriteLine("Para la segunda puerta..."); p2.MostrarEstado(); } }
Nota: al igual que existen los "constructores", también podemos indicar un "destructor" para una clase, que se encargue de liberar la memoria que pudiéramos haber reservado en nuestra clase (no es nuestro caso, porque aún no sabemos manejar memoria dinámica) o para cerrar ficheros abiertos (que tampoco sabemos).
Un "destructor" se llama igual que la clase, pero precedido por el símbolo "~", no tiene tipo de retorno, y no necesita ser "public", como ocurre en este ejemplo:
~Puerta() { // Liberar memoria // Cerrar ficheros }