Miércoles 18 de Agosto

Abstracción vs. ausencia de abstracción

Objetivos: Temas:


Repaso: Uso de objetos como records

Cuando se desea usar los objetos como records, las clases que definen estos objetos no llevan métodos. Sólo se declaran las variables de instancia que se incluyen en cada objeto, es decir cada record.

Cuando se tiene una referencia de un record, sus variables de instancia se leen y modifican directamente. Es decir que el programa que usa los records usa directamente la representación de los records.

Uso de objetos como tipos de datos abstractos

Cuando los programas son grandes, es recomendable usar los objetos como tipos de datos abstractos. Un tipo de datos abstracto (T.D.A.) se manipula a través de un conjunto bien definido de operaciones. Por ejemplo, Ud. ha usado las clases Map, TextReader, Tortuga, etc. por medio de sus operaciones sin tener idea de qué variables de instancia se utilizan para representar esos objetos y como están programados (implementados) sus métodos.

Por eso se habla de abstracción: el programa que usa los objetos como tipos de datos abstractos, se abstrae de su implementación.

Los tipos de datos primitivos en Java como int, double y boolean también son tipos de datos abstractos, porque Ud. no necesita saber como se representan para trabajar con ellos, ni saber como se implementan la suma, multiplicación, división en los circuitos del computador.


Programas sin abstracción

En una clase anterior definimos la clase Tiempo. Sus objetos se usaron como tipos de datos abstractos. ¿Cómo se vería un programa que suma tiempos usando los objetos como records?

    Tiempo t1= new Tiempo();
    print("Horas 1 ? ");
    t1.horas= readInt();
    print("Minutos 1 ? ");
    t1.min= readInt();

    Tiempo t2= new Tiempo();
    print("Horas 2 ? ");
    t2.horas= readInt();
    print("Minutos 2 ? ");
    t2.min= readInt();

    t1.horas= t1.horas+t2.horas+(t1.min+t2.min)/60;
    t1.min= (t1.min+t2.min)%60;
    print("Suma= "+t1.horas+":"+t1.min);
en donde la clase Tiempo ha sido definida al final del archivo como:

    class Tiempo {
      int horas;
      int min;
    }
La desventaja de esta forma de programar (sin abstracción) es que si deseamos cambiar la representación del tiempo por una representación que sólo contenga minutos, hay que cambiar todos los lugares del programa en donde se haya usado la clase Tiempo.


Abstracción mediante objetos

En la clase pasada resolvimos el problema de ordenar un arreglo de objetos usados como records. Ahora resolveremos el mismo problema usando los objetos como tipos de datos abstractos. Es decir definiremos los métodos adecuados para la clase Post y haremos el ordenamiento sin conocer cuál es la representación de estos objetos.

Dicho de otra forma, descompondremos el problema inicial en (i) un programa que construye el arreglo de objetos, lo ordena y luego lo escribe en otro archivo, y (ii) la clase Post con operaciones sobre los postulantes.

Cuando se define una clase, se debe decidir qué operaciones se necesitan para esa clase. Inspeccionando el programa original, nos damos cuenta que se necesita leer un postulante de un archivo, compararlo en el orden lexicográfico con otro postulante, desplegar sus datos en pantalla y escribirlo en disco.

Con esas operaciones podemos reescribir el programa que lee el archivo y lo ordena por nombres, de modo que este programa nunca manipule directamente las variables de instancia de un objeto de la clase Post.

    // Leer de "post.dat"
    int npost= ...; // el numero de postulantes en el archivo
    Post[] posts= new Post[npost];
    TextReader lect= new TextReader("post.dat");
    int i= 0;
    while (true) {
      String lin= lect.readLine();
      if (lect.eofReached())
        break;
      posts[i]= new Post(lin);

      i= i+1;
    }
    // Ordenar
    ordenar(posts, npost);
    // Escribir en "post2.dat"
    TextWriter escr= new TextWriter("post2.dat");
    i= 0;
    while (i<npost) {
      posts[i].escribir(escr);
      i= i+1;
    }
y el método ordenar quedaría prácticamente igual. Sólo cambia la comparación entre nombres de postulantes porque en la solución original se accesan sus variables de instancia:

    void ordenar(Post[] a, int n) {
      ...
      if (a[i].compararCon(a[i+1])>0) { ... }
      ...
    }
Una vez determinados cuáles son los métodos que necesita la clase Post, volvemos a definirla:

    class Post extends Program {
      String ci;
      String nombre;
      String ptje;
      Post(String lin) {
        FieldParser decod= new FieldParser(lin, ":");
        this.ci= decod.readString();
        this.nombre= decod.readString();
        this.ptje= decod.readInt();
      }
      void escribir(TextWriter escr) {
        escr.println(this.ci+":"+this.nombre+":"+this.ptje);
      }
      int compararCon(Post post) {
        return compare(this.nombre, post.nombre);
      }
    }
Observe que al implementar las operaciones de la clase Post, se hace indispensable accesar sus variables de instancia. Esa es la idea de la abstracción: postergar al máximo el acceso a las variables de instancia. Lo importante es que los programas que usan la clase Post no accesen directamente las variables de instancia.


Ventajas de la abstracción

Ud. sólo podrá apreciar las bondades del diseño del programa anterior resolviendo los siguientes problemas.

Ejercicio 1:

El archivo "post.dat" ahora tiene como cuarto campo la dirección del postulante. Modifique la clase Post de modo que ahora se tenga en cuenta la presencia de la dirección.

Este ejercicio permite apreciar que no es necesario cambiar una coma del programa que usa la clase Post. El programador se concentra sólo en la definición de la clase Post.

Ejercicio 2:

Ahora se necesita que el archivo "post2.dat" quede ordenado por puntaje descendentemente y no ascendentemente por nombre. Sólo modifique la clase Post para tener en cuenta este nuevo requerimiento (no puede modificar el programa que usa la clase Post).

Este ejercicio permite apreciar nuevamente que no es necesario alterar el programa que usa la clase.

Ejercicio 3:

El número de postulantes ha aumentado considerablemente haciendo que el algoritmo de ordenamiento sea muy lento. Se requiere programar un nuevo método de ordenamiento más eficiente. Modifique el procedimiento ordenar para que utilice el algoritmo quicksort (para esto introduzca un nuevo procedimiento que ordene un rango de índices en el arreglo).

El ejercicio permite apreciar que podemos cambiar el algoritmo de ordenamiento sin modificar la lectura de los postulantes, su escritura o la clase Post.

Conclusión:

El beneficio de la abstracción es doble: Primero permite disminuir la complejidad de los problemas ignorando los detalles de la resolución del problema completo. Y segundo, permite cambiar componentes de la solución para adaptarse a nuevos requerimientos del problema.

La desventaja de la abstracción es que no es fácil darse cuenta en donde hay que establecer los límites de cada subproblema de modo que se minimicen las operaciones que se necesitarán.


Supresión del identificador this

Al definir una clase, en la mayoría de las ocasiones no es necesario colocar el identificador this porque el compilador lo puede agregar automáticamente. La clase Post se podría reescribir como:

    class Post extends Program {
      String ci;
      String nombre;
      String ptje;
      Post(String lin) {
        FieldParser decod= new FieldParser(lin, ":");
        ci= decod.readString();
        nombre= decod.readString();
        this.ptje= decod.readInt();
      }
      void escribir(TextWriter escr) {
        escr.println(ci+":"+nombre+":"+this.ptje);
      }
      int compararCon(Post post) {
        return compare(nombre, post.nombre); // (*)
      }
    }
Observe que en (*) se necesita accesar el nombre de un objeto que no es this. En este caso es necesario entonces colocar el prefijo post. para diferenciarlo del acceso a las variables de this.