Encapsulamiento y visibilidad

video[Tutorial] Encapsulamiento y visibilidad en Java

Para un diseño adecuado del software resulta imprescindible un correcto encapsulamiento del código. El mayor problema reside en que el software tiende a cambiar con mucha facilidad, (siempre encontramos mejores formas de resolver un problema) y resulta imprescindible que estos cambios afecten lo menos posible a otras partes de código. Pasemos a definir algunos términos de forma más precisa.

Cuando diseñamos un software hay dos aspectos que resultan fundamentales:

Interface: Cómo este software puede ser utilizado.

Implementación: El código utilizado para resolver los distintos algoritmos.

El concepto de interfaz se define en Java como los elementos de una clase que son visibles desde fuera de esta (con visibilidad public). La implementación se define creando unos determinados atributos y escribiendo el código de los diferentes métodos.

El encapsulamiento consiste en ocultar la implementación y los atributos de un objeto, de manera que sólo se puede cambiar su estado mediante ciertas operaciones definidas en el interface del objeto. Dicho de otra forma, procurar que el interface sea lo más independiente posible de la implementación.

En Java el encapsulamiento está estrechamente relacionado con la visibilidad. Para indicar la visibilidad de un elemento (tanto atributos como métodos) podemos anteceder una de las siguientes palabras reservadas:

public: accesibles desde cualquier clase.

private: sólo son accesibles por la clase actual.

protected: sólo por la clase actual, sus descendientes y clases de nuestro paquete.

<si no indicamos nada> sólo son accesibles por clases de nuestro paquete.

Acceso a los atributos de la clase

Los atributos de una clase están estrechamente relacionados con su implementación. Por esta razón conviene marcarlos como private e impedir su acceso desde fuera. De esta forma en un futuro podremos cambiar la representación interna del objeto sin alterar su interface.

Aunque en la teoría esta afirmación parece lógica, en la práctica resulta difícil de seguir. Pensemos en las dos clases que hemos diseñado. En la de los números complejos al haber puesto private en los atributos no vamos a permitir que acceder la parte entera e imaginaria del objeto:

class Complejo {
    private double real, imaginario;
    …

Lo mismo ocurre con longitud y latitud de una coordenada geográfica. Por lo tanto, resulta imposible consultar o modificar esta información desde fuera de la clase. Para solucionar este problema vamos a utilizar los métodos getters y setters (obtención y establecimiento). Estos métodos son utilizados para consultar el valor de un atributo (getter) o para modificarlo (setter). Resulta imprescindible indicar que tienen una visibilidad public para que puedan ser invocados desde fuera de la clase. Por ejemplo, para el atributo real escribiríamos los métodos:

public double getReal() {
   return real;
}

public void setReal(double real) {
   this.real = real;
} 

Esta forma de trabajar puede parecer algo engorrosa, pero tiene sus ventajas:

  • Como veremos en el siguiente apartado, podemos cambiar la representación interna de la clase sin alterar el interface
  • Verificar que los valores son correctos:
public void setReal(double real) {
   if (real>100000) {lanzamos una excepción}
   else this.real= real;
}
  • Modificar otros aspectos del objeto o lanzar eventos:
public void setReal(double real) {
   contadorModificaciones++; //Con fines estadísticos
   lanzamos el evento: onComplejoChange
   this.real= real;
}

Resulta tan frecuente su utilización que Eclipse incorpora una herramienta para crearlos de forma automática. Realiza el siguiente ejercicio para aprender su funcionamiento:

Ejercicio paso a paso: Creación automáticas de getters y setters

1.     En el  proyecto Complejos, abre la clase Complejo.

2.     Pulsa con el botón derecho sobre el código y selecciona la siguiente opción: con Android Studio  Generate.../ Getter and Setter  y con Eclipse Source / Generate Getters and Setters...

3.     Selecciona los dos atributos de la clase y pulsa OK. Verás cómo se inserta el siguiente código:

public double getReal() {
   return real;
}
 
public void setReal()double real){
   this.real = real;
}
 
public double getImaginario(){
   return imaginario;
}
 
public void setImaginario(double imaginario) {
   this.imaginario = imaginario;
}

4.     Pulsa en el botón Guardar para almacenar el fichero.

Practica: Getters y setters en la clase coordenadas geográficas

Inserta en la clase GeoPunto los getters y setters necesarios para acceder a sus atributos.

Cambio de la representación interna de una clase

Tras insertar los cuatro métodos del ejercicio anterior, uno podría pensar que el resultado es el mismo que si hubiéramos puesto public delante de los dos atributos. No es exactamente así, imaginemos que más adelante queremos verificar que la latitud de un GeoPunto esté en el rango [-90, 90], y en caso contrario lanzar una excepción. Si los usuarios de la clase acceden directamente al atributo esto no será posible. Más todavía, imagina que en un momento determinado queremos cambiar la representación interna de un número complejo para utilizar módulo y fase en lugar de parte entera y parte imaginaria. Si hemos tenido la precaución de no publicar los atributos, será posible realizar este cambio sin cambiar el interface de la clase.

class Complejo {
//declaración de atributos
private double modulo, fase;
 
//declaración de constructor
public Complejo(double real, double imaginario) {
   modulo = Math.hypot(real, imaginario);
   fase = Math.atan2(imaginario, real);
}
 
//declaración de métodos
public double getReal() {
   return modulo * Math.cos(fase);
}
…

Cuando decimos que no cambiamos el interface de la clase, queremos decir que no hemos modificado ninguna de las llamadas marcadas como public. Por lo tanto, no tendremos que modificar ninguna línea del código que utiliza la clase Complejo. Aunque la forma interna de trabajar ha cambiado radicalmente. En la nueva implementación algunas operaciones son mucho más rápidas (como obtener el módulo), pero otras son mucho más lentas (como la suma). Si quieres puedes ver el ejemplo completo de cómo se podría implementar esta clase en el siguiente fichero.

Practica: Cambio de representación de la clase coordenadas geográficas

Modifica la clase GeoPunto para que los atributos longitud, latitud sean representados mediante un int en lugar de con double. Para disponer de una precisión adecuada ahora representarán millonésimas de grado en lugar de grados. De esta forma tenemos una representación más compacta. Como veremos más adelante esta representación es la utilizada en el API de Google Maps. IMPORTANTE, el interfaz de la clase ha de permanecer exactamente igual. Para el constructor puedes utilizar el siguiente código.

public GeoPunto(double longitud, double latitud) {
   this.longitud = (int) (longitud * 1E6);
   this.latitud = (int) (latitud * 1E6);
}