Miércoles 29 de Septiembre

Encapsulación

Objetivos:

Temas:


Patrones de diseño: plantilla

Un patrón de diseño es una estrategia aplicada frecuentemente para descomponer un problema complejo en problemas más simples. A modo de ejemplo, en la soluciones del problema del ordenamiento y el del cálculo del área bajo la curva se aplicó un patrón de diseño denominado plantilla.

Este patrón se usa cuando se desea escribir un algoritmo que se puede aplicar a distintos tipos de datos, pero en donde el algoritmo depende de operaciones que varían según el tipo específico de los datos a los que se aplica ese algoritmo.

Por ejemplo, si se tiene una algoritmo A que depende de operaciones variables m1 y m2:

    void A(...){
      ...  m1(...); ... m2(...); ...
    }
Entonces para hacer que este mismo código se pueda aplicar a los distintos tipos de datos, el método se coloca en una clase CalcA. Los definiciones específicas de los métodos m1 y m2 se colocan en subclases de CalcA:

    class CalcA extends Program {
      void A(...){
        ...  m1(...); ... m2(...); ...
      }
      void m1(...) { }
      void m2(...) { }
    }
en donde los métodos m1 y m2 se dejan en blanco. Por supuesto esta es la forma general del patrón, pero al igual que cualquier otro patrón de programación, admite una infinidad de variantes para adaptarlo al caso específico de diseño que se pretende resolver. En ocasiones resulta conveniente agrupar varios algoritmos en una sola clase. Por ejemplo la clase Función podría no sólo servir para calcular el área bajo la curva si no que también para calcular sus ceros, graficarla, etc.:

    class Funcion extends Program {
      double areaTrapecios(double a, double b, int n) { ...  }
      double areaSimpson(double a, double b, int n) { ...  }
      void graficar(Pizarra p, double a, double b, ...) { ... }
      ...
    }

Diseño y abstracción

Como hemos insistido, el diseño de una solución consiste en descomponer el problema en subproblemas más simples. Una forma de realizar esta descomposición es asignar cada subproblema a una clase. Los programadores que usan una clase aplican abstracción, despreocupándose de los detalles de como se implementan los métodos que invocan.

Cuando los proyectos son grandes, como para que el desarrollo de un programa abarque varios meses e involucre muchos programadores, la metodología puede fallar por falta de abstracción por parte de algunos programadores. Para entender mejor el problema examinaremos el siguiente escenario: un programador requiere realizar una operación con un objeto, pero esta operación no ha sido definida en la clase a la que pertenece ese objeto, aunque se puede llevar a cabo con la información que se mantiene en las variables de instancia.

Como normalmente los programadores pueden examinar el código que ha escrito otro programador, el programador que usa una clase se ve tentado por realizar la operación accesando directamente las variables de instancia del objeto. Este acceso podría ocurrir en cualquier parte del programa completo. Este es un atajo que permite resolver rápidamente el problema. Sin embargo, más tarde el programador que ha implementado la clase podría decidir introducir cambios en las variables de instancia, provocando que el código del primer programador deje de funcionar.

Si se abusa de este tipo de atajos en un programa de gran envergadura, éste se puede tornar incomprensible para todo el grupo. Simplemente, ningún programador puede modificar el código que ya escribió, porque esto haría que el todo deje de funcionar.

La forma correcta de proceder en estos casos es agregar la operación a la clase y en lo posible accesar las variables de instancia en la misma clase, de modo que si se introducen modificaciones en las variables de instancia, será evidente que hay que cambiar el nuevo método, gracias a su cercanía con el resto de los métodos. Una de las gracias del mecanismo de clases es que permite concentrar todos los posibles usos de las variables de instancia en un solo archivo, evitando así que queden diseminados por todas partes. La única forma de operar con las variables de instancia debería ser por medio de los métodos de la clase.

La modificación de una clase la puede realizar tanto el programador que usa la clase como el que la definió, pero lo importante es que la operación quede definida en la misma clase en donde se han declarado las variables de instancia.


Encapsulación

Para encausar a los programadores hacia el correcto uso del mecanismo de clases, algunos lenguajes de programación ofrecen herramientas para prohibir el acceso a las variables de instancia de una clase. La idea es que los programadores agreguen el atributo private a las variables de instancia:

    class Tiempo extends Program {
      private int horas;
      private int min;
      ...
    }
Con este atributo, si un programador quisiera accesar las variables de instancia horas y min fuera de la clase, se producirá un error de compilación:

    class Uso extends Program {
      void run() {
        Tiempo t= new Tiempo(10, 20);
        t.horas= -4; // error en tiempo de compilación
        ...
      }
    }
De esta forma, a futuro se podrá cambiar la representación de los objetos de la clase Tiempo, asegurándose que ninguna clase usuaria de Tiempo se verá afectada por el cambio.


Encapsulación por medio de paquetes de clases

El mecanismo de encapsulación por medio del atributo de privacidad no es suficiente. Existen muchísimos problemas en donde se hace necesario definir dos clases que se accesan mutuamente las variables de instancia. Esto impide que se puedan declarar sus variables de instancia como privadas. Y por lo tanto un programador usuario de estas clases podría accesar esas variables desde una tercera clase, lo cual nos gustaría evitar.

Para ello Java ofrece el mecanismo de encapsulación por medio de paquetes de clases. La idea es que fuera de un paquete de clases sólo es posible accesar las variables de instancia y métodos que hayan sido definidos con el atributo public. Los programadores del paquete de clases especifican como públicas sólo aquellas clases y operaciones contempladas para los usuarios del paquete.

A modo de ejemplo, supongamos que se desea empaquetar las clases para realizar animaciones en un paquete llamado anim. Para lograr esto será necesario indicar al comienzo de cada archivo que las clases contenidas pertenecen al paquete anim:

    package anim;
Luego, se definen las clases señalando las clases y métodos que son públicas. Es decir que se pueden usar fuera del paquete:

    package anim;

    public class Glyph extends Program {
      int x, y;
      ...
      public int getX() { ... }
      public int getY() { ... }
      public void moveTo(int x, int y) { ... }
      void draw(Pizarra p) { ... }
    }
Observe que sólo se declaran públicos los métodos getX, getY y moveTo, por lo que podrán ser usados fuera del paquete (o desde otros paquetes). En cambio el método draw no es público y por lo tanto no puede ser usado por clases que están fuera del paquete anim.

    package anim;

    public class Animator extends Program {
      Pizarra p;
      public void drawAndSleep(...) {
        ...
        gl.draw(p);
        ...
      }
      ...
    }
Aunque un programador puede examinar el código y saber que existe el método draw y las variables de instancia x, y y p, no podrá utilizarla porque el compilador se lo impedirá, entregando un mensaje de error en caso que se usen fuera del paquete.

La variables de instancia pueden ser declaradas como públicas pero un buen estilo de programación es que no lo sean. Si se requiere conocer su valor desde fuera del paquete, se definen métodos que entregan su valor (como getX y getY). Pero como no hay método para obtener la pizarra asociada a un objeto de la clase Animator, no será posible dibujar cosas distintas de un glyph en esa pizarra.


Uso de paquetes de clases

Para usar las clases que se ubican en un paquete, normalmente se usa la directiva import:

    import anim;

    ...
Observación:

Como no es un objetivo de este curso que los alumnos realicen programas de gran envergadura como para que se hagan necesarios los mecanismos de encapsulación, no seguiremos profundizando al respecto. Sólo utilizaremos la palabra public a título de comentario para señalar que un método puede ser invocado por los usuarios de la clase.

Debe quedar claro que el uso de public es inútil cuando las clases no se han organizado en paquetes. Por otra parte, en programas pequeños resulta sencillo ser disciplinados en cuanto a la abstracción y por lo tanto los mecanismos de encapsulación pierden efectividad.