Miércoles 28 de Julio

Definición de Clases - 2da. Parte

Objetivos: Mostrar los beneficios que ofrece la descomposición de los programas complejos en múltiples clases.

Temas:


Resumen de la clase anterior

Los objetos de una misma clase poseen, en general, atributos o propiedades distintas. Esta diferenciación hace que realicen acciones distintas. Por ejemplo, dos colas pueden almacenar elementos distintos y por lo tanto al extraer el primer elemento de ambas colas, se obtendrán valores distintos, aún cuando pertenecen a la misma clase.

Las propiedades de los objetos se almacenan en las variables de instancias. Estas se crean al construir el objeto y se inicializan en el constructor. Las variables de instancia se destruyen junto con el objeto, cuando éste pierde su utilidad.

Al definir una clase es necesario indicar cuales serán las variables de las instancias (i.e. los objetos). Por ejemplo:

    class Tiempo {
      int horas;
      int min;
      ...
    }

Las variables de una instancia se puede obtener o modificar teniendo una referencia de esa instancia. Por ejemplo:

    int h= t.horas; // Obtiene el valor de la variable horas
    t.min= 15;      // Modifica el valor de la variable min
Al definir una clase también se especifican sus métodos, que son los que se encargan de manipular las variables de instancia. Los métodos son en esencia funciones o procedimientos como los que hemos visto hasta ahora. La diferencia está en que los métodos se invocan adjuntando un objeto que constituye el objeto de la invocación. Por ejemplo en:

    t.sumar(periodo);
el objeto de la invocación es el objeto referenciado por t.

En la definición de un método, el objeto de la invocación es un parámetro implícito, porque no es necesario declararlo. Este parámetro es accesible por medio de la variable this:

    class Tiempo {
      ...
      void sumar(Tiempo t2) {
        this.horas= ... this.horas ... t2.horas ...
        ...
      }
    }

Definición del constructor

El constructor es un pseudo método que lleva el mismo nombre de la clase y no posee tipo de retorno. Se invoca automáticamente al crear un objeto con new. En él, se inicializan las variables de instancia del nuevo objeto. En la clase Tiempo el constructor es:

      Tiempo(int h, int m) {
        this.horas= h;
        this.min= m;
      }
En él, se asignan los valores iniciales para las variables de instancia this.horas y this.min. Los valores que se le asignan se reciben como parámetros.

El siguiente ejemplo permite visualizar mejor los eventos que ocurren las construir un objeto de la clase Tiempo. Supongamos que se ejecuta la siguiente instrucción:

    Tiempo t= new Tiempo(8, 0);
El operador new Tiempo(...) hace que se construya un nuevo objeto de la clase Tiempo, creando variables de instancia para él. En seguida se invoca el constructor, asignando a h el primer argumento especificado (8) y a m el segundo argumento (0). Luego se ejecutan las instrucciones del constructor.

Una vez terminada la ejecución del constructor, el operador new entrega una referencia del objeto recién construido, que finalmente es asignada a la variable t.


Beneficios de la definición de clases

El diseño de la solución de un problema consiste en descomponerlo en subproblemas más simples. Una forma avanzada de diseñar soluciones es lograr que cada subproblema sea resuelto por una clase de objetos. Cada clase resuelve una parte del problema abstrayéndose de los detalles de la resolución de todo el problema.

Al trabajar con clases de objetos es importante distinguir entre (i) el empleo de una clase y (ii) la definición o implementación de esa clase. Una clase se emplea al resolver otras partes del problema y por lo tanto, sus objetos se manipulan únicamente invocando sus métodos. Nunca se accesan directamente sus variables de instancia, porque éstas son parte de la implementación de la clase. Accesar las variables de instancia al emplear una clase es una violación del principio de abstracción.

En cambio cuando se define la clase, el programador está precisamente resolviendo el subproblema que se ha delegado a esa clase. Y por lo tanto es necesario accesar sus variables de instancia, pero también se pueden invocar su métodos.

Esta forma de trabajar con las clases es importante porque a menudo uno se encuentra con la necesidad de cambiar la representación de los objetos (es decir, cambiar sus variables de instancia) y los algoritmos que se emplean en la clase, sin cambiar la forma en que se emplean estos objetos.

Por ejemplo, otra otra forma de implementar la clase Tiempo consiste en representar el tiempo en términos de minutos (que pueden exceder la hora):

    class Tiempo {
      // Variables de instancia:
      int min;
      // El constructor:
      Tiempo(int h, int m) {
        this.min= h*60+m;
      }
      // Los métodos:
      void sumar(Tiempo t2) {
        this.min= this.min+t2.min;
      }
      void imprimir() {
        println((this.min/60)+":"(this.min%60));
      }
      int comparar(Tiempo t2) {
        if (this.min<t2.min)
          return -1;
        if (this.min==t2.min)
          return 0;
        return 1;
      }
    }
Los programas que emplean esta nueva definición de la clase Tiempo no necesitan alterarse. Seguirán funcionando correctamente.


Ejercicio: Adivina mi número

Se dispone de una clase que juega al adivina mi número. Los objetos de esta clase pueden adivinar el número del usuario, pero no saben dialogar con el usuario. Los objetos poseen los siguientes métodos:

Ejemplo Significado Declaración
Adivino a= new Adivino(0, 1023); Construye un adivino Adivino(int min, int max)
int n= a.jugar(); Juega el número n int jugar()
a.mayor(n); se le indica que es mayor que n void mayor(int n)
a.menor(n); se le indica que es menor que n void menor(int n)
int cont= a.intentos(); entrega cuantos intentos ha hecho int intentos()

El ejercicio consiste en emplear esta clase para escribir un programa que entable el siguiente diálogo con el usuario:

   Piense un número entre 0 y 1023 y trataré de adivinarlo.
   Esta listo ?
   Digame si es menor, mayor o igual que 512 ? menor
   Digame si es menor, mayor o igual que 200 ? mayor
   Digame si es menor, mayor o igual que 400 ? mayor
   ...
   Digame si es menor, mayor o igual que 415 ? igual
   Ok, lo adivine en x intentos
Supuestos: (a) el usuario no hace trampas, y (b) la clase Adivino se encarga de la estrategia de juego.

Solución:

   println("Piense un número entre 0 y 1023 y trataré de adivinarlo.");
   println("Esta listo ? ");
   readLine(); // Espera que el usuario ingrese una línea
   Adivino adiv= new Adivino(0, 1023);
   while(true) {
     int n= adiv.jugar();
     println("Digame si es menor, mayor o igual que "+n+" ? ");
     String resp= readLine();
     if (compare(resp, "igual")==0)
       break;
     if (compare(resp, "menor")==0)
       adiv.menor(n);
     else
       adiv.mayor(n);
   }
   println("Ok, lo adivine en "+adiv.intentos()+" intentos");
Al resolver este problema nos hemos abstraído de la parte difícil que consite en adivinar el número. La inteligencia para adivinar el número la hemos puesto en el objeto adivino.


Tarea

Escriba una versión de la clase Adivino que siga una estrategia de juego trivial: juegue sistemáticamente el mínimo valor posible. Cuando se le diga que el número del usuario es mayor que el número jugado, incremente el mínimo en 1.