Programación en Java SE 6 desde cero: Parte 5
CONCEPTOS DE ORIENTACIÓN A OBJETOS
Objetivos
Comprender la metodología de programación Orientada a Objetos con ejemplos en Java.
Estudiar los términos con sus correspondientes usos de: encapsulación, cohesión, polimorfismo y acoplamiento.
Identificar las posibles relaciones entre las clases Java.
Encapsulacion en JAVA
La encapsulación se refiere a la combinación de atributos y métodos todo junto en una clase. Los métodos hace referencia al comportamiento del objeto y los atributos a su estado.
En orientación a objeto se habla de encapsulación pero se refiere habitualmente a un concepto más estricto, por el cual se estable como norma que los atributos (datos o estado de un objeto) solo se hace accesibles a través de los propios métodos del objeto. Evitando así el libre acceso y modificación de los mismos. En concreto es una cuestión de responsabilidades, es decir el objeto que guarda un estado es responsable siempre de ese estado.
A nivel de programación realizamos eta encapsulación estricta haciendo que los atributos de una clase tengan el modificador de acceso private, con esto conseguimos que ningún objeto externo tenga acceso a los datos que gestiona las clase que estamos programando. Entonces, ¿Qué hacemos para acceder a estos datos?
Dentro de la clase que estamos programando generamos métodos para leer los datos y modificarlos.
Métodos Set y Get en JAVA
En Java de forma estándar se llama a estos métodos setter, para los de modificación de datos y getter a los que nos permiten recuperar estos valores,
public class Libro {
private int numeroPaginas;
private String titulo;
public String getTitulo() {
return this.titulo;
}
public void setTitulo(String nuevoTitulo) {
this.titulo = nuevoTitulo;
}
public int getNumeroPaginas() {
return this.numeroPaginas;
}
public void setNumeroPaginas(int nuevoNumeroPaginas) {
this.numeroPaginas = nuevoNumeroPaginas;
}
}
En el ejemplo podemos ver la implementación de una clase bajo estas normas de encapsulación estricta.
Seguir estas buenas prácticas y conceptos de orientación a objetos en cuanto a la encapsulación tiene ciertos beneficios:
Podremos monitorizar y validar todos los cambios que se realicen sobre los atributos contenidos.
De la misma forma podremos formatear cualquier contenido de entrada o salida.
También es posible independizar al contexto externo al objeto de tipo de dato con el que se trabaja internamente.
Codificación de los métodos Set y Get en JAVA
Con este tipo de codificación para acceder a los datos de la clase Libro es necesario pasar por sus métodos getter y para modificarlos por los setter.
public class PrincipalEncapsulacion {
public static void main(String[] args) {
Libro quijote = new Libro();
quijote.setTitulo(“El quijote”);
quijote.setNumeroPaginas(523);
}
}
Aquí podemos observar como siguiendo estas normas conseguimos centralizar el acceso a los datos, por lo tanto cualquier clase para establecer el número de páginas o título de la clase Libro debe hacerlo con sus métodos y no de forma directa si el atributo hubiera sido público. Imaginemos ahora que cambian las condiciones de nuestro negocio y la librería para la que estamos realizando este programa y una de las restricciones es que no se pueden tener libros de más de quinientas páginas. Si no hubiéramos utilizado la encapsulación estricta tendríamos que buscar por todo nuestro código los puntos donde se accede a este dato y realizar la nueva codificación, con lo cual estaríamos duplicando líneas de código por diferentes sitios en nuestro proyecto, mientras que utilizando esta buena práctica de diseño únicamente nuestro código se modificaría como se ve en el ejemplo:
public class Libro {
private int numeroPaginas;
private String titulo;
public String getTitulo() {
return this.titulo;
}
public void setTitulo(String nuevoTitulo) {
this.titulo = nuevoTitulo;
}
public int getNumeroPaginas() {
return this.numeroPaginas;
}
public void setNumeroPaginas(int nuevoNumeroPaginas) {
if (nuevoNumeroPaginas > 500) {
// Generar el codigo de negocio necesario
}
this.numeroPaginas = nuevoNumeroPaginas;
}
}
Como se observa sólo hemos modificado el método de establecimiento de datos en la clase que encapsula el dato numeroPaginas y hemos conseguido automáticamente que cualquier clase que estuviera utilizando este dato tenga una restricción de acceso para libros mayores de quinientas páginas.
Esto ahorra líneas de código duplicadas y hace que en todo momento sepamos donde debemos realizar los cambios en los programas, optimizando el tiempo de desarrollo y la mantenibilidad de nuestros proyectos.
Coupling en JAVA
El acoplamiento (o Couplin) se refiere a las dependencias que tienen unas clases de otras. El grado de acoplamiento puede ser alto o bajo, cuanto más bajo sea el grado de acoplamiento de nuestras clases mejor, ya que esto proporciona independencia entre las clases de código y las modificaciones generan menor impacto en nuestro código, mientras que en el caso contrario, con un alto acoplamiento las modificaciones en unas clases harán que tengamos que modificar mucho código de nuestro proyecto.
Seguir un diseño que tenga bajo acoplamiento también nos proporciona la posibilidad de tener un mayor grado de reutilización de nuestras clases en otros proyectos.
Veamos el siguiente ejemplo para aclarar mejor estos conceptos de acoplamiento.
Imaginemos que tenemos una clase Cliente, esta agrega una clase Coche y la clase Coche agrega una clase Fabricante, y esta ultima tiene un atributo nombre, que queremos visualizar desde la clase inicial Cliente.
Podríamos generar un código con acoplamiento, como este que veremos, que se encuentra en la clase Cliente:
Fabricante fabricante = this.coche.fabricante;
System.out.println(fabricante.getNombre());
El acoplamiento se ha generado en la clase Cliente con Fabricante, veamos como se generaría un código sin acoplamiento, dentro de la misma clase Cliente:
System.out.println(this.coche.getFabricante().getNombre());
Podemos observar como el resultado del método es exactamente el mismo, hemos sacado por pantalla el nombre del fabricante del automóvil pero sin generar dependencias entre la clase Cliente y Fabricante.
Cohesión en JAVA
La cohesión se refiere a las responsabilidades que le otorgamos a una clase, o si lo traducimos a programación es a la cantidad de tareas que hace una clase. Nos referimos a alta cohesión cuando una clase realiza un número de tareas relativos siempre a una misma funcionalidad, y baja cohesión cuando se realizan muchas tareas y no están relacionadas unas con otras. Por lo tanto la buena práctica es conseguir un código que tenga alta cohesión.
Los proyectos con alta cohesión como los que queremos conseguir, normalmente se diferencian porque tienen un mayor número de clases y cada una realiza un conjunto de tareas pequeño, suelen ser clases especializadas. En cambio los proyecto con baja cohesión (mala práctica), suelen ser del tipo de pocas clases con muchas líneas de código.
Los beneficios de conseguir una alta cohesión son similares a los anteriores, nuestro proyecto será más sencillo de mantener y realizar modificaciones y adaptaciones.
Imaginemos una clase RecursosHumanos que realiza un montón de tareas relacionadas (métodos).
public class RecursosHumanos {
public void pagarNomina() {
// negocio
}
public void pagarImpuestos() {
// negocio
}
public void contratarEmpleado() {
// negocio
}
public void despedirEmpleado() {
// negocio
}
}
Podemos decir que esta con bajo acoplamiento ya que tiene un montón de tareas funcionales diferentes, como son pagar las nominas e impuestos y contratación de empleados, por lo tanto para generar alto acoplamiento generaríamos dos clases que cada una se encargará de sus tareas específicas, Nominas y Contratacion.
public class Nominas {
public void pagarNomina() {
// negocio
}
public void pagarImpuestos() {
// negocio
}
}
public class Contratacion {
public void contratarEmpleado() {
// negocio
}
public void despedirEmpleado() {
// negocio
}
}
Polimorfismo en JAVA
Este concepto está referido a como un objeto en Java puede adoptar múltiples formas. El polimorfismo es el resultado de la herencia y de la implementación de interfaces:
- Una clase derivada o hija, toma la forma de su padre.
- Una clase coge la forma de la interfaz que implementa.
Para comprender como funciona en Java vamos a ver un ejemplo donde tenemos una interfaz InstrumentoMusical y una clase Flauta que implementa dicha interfaz:
public interface instrumentoMusical {
public void tocarInstrumento();
}
public class Flauta implements InstrumentoMusical {
public void tocarInstrumento() {
System.out.println("Suena la flauta... flu flu flu");
}
}
Es muy útil y ayuda mucho, el concepto “es un/a” cuando hablamos de un objeto, en este caso, debido a la implementación de la interfaz podemos decir que la clase Flauta es un InstrumentoMusical. Veamos en que se traduce las posibilidades de esto en Java con un código de ejemplo:
public class PrincipalOrquesta {
public static void main(String args[]) {
Flauta miFlauta = new Flauta();
InstrumentoMusical instrumento = miFlauta;
instrumento.tocarInstrumento();
miFlauta.tocarInstrumento();
}
}
Hemos visto como con dos tipos de variable diferente uno de tipo Flauta y otro de tipo InstrumentoMusical tenemos la referencia a la misma variable y podemos trabajar con sus métodos. Por lo tanto hemos conseguido que el objeto miFlauta adopte diferentes formas, la de Flauta y la de InstrumentoMusical.
En el caso de la herencia extendiendo de una clase, ya sea abstracta o no, es exactamente igual que este caso, una clase puede adoptar la forma de su padre o de cualquier clase que se encuentre en la cadena de herencia hacia arriba, pero no hacia abajo, y para esto debemos apoyarnos en nuestro lenguaje: “Una Flauta es un InstrumentoMusical”, esto es cierto en cualquier caso, por lo tanto una Flauta puede tomar la forma de un InstrumentoMusical como hemos visto en el ejemplo, pero “un InstrumentoMusical no siempre es una Flauta”, ya que puede haber otras clases que implementen la interfaz, como un piano o una guitarra, y hay que utilizar el Casting para forzar esta transformación (se verá a continuación).
Cuando utilizamos las diferentes formas de un objeto (polimorfismo) debemos tener en cuenta en función de la forma que adoptemos disponemos de unos métodos u otros. En nuestro ejemplo podemos adoptar la forma InstrumentoMusical y Flauta, que tienen los mismos métodos, pero veamos esta nueva implementación de Flauta:
public class Flauta implements InstrumentoMusical {
public void tocarInstrumento {
System.out.println("Suena la flauta flu flu flu");
}
public void cambiarBoquilla() {
System.out.println("Cambiamos la boquilla");
}
}
Ahora aunque el objeto que estamos utilizando sea el mismo desde los variables diferentes, en función de la forma que adoptemos tendremos disponibles unos métodos u otros. Para ello hacemos unas modificaciones en la clase PrincipalOrquesta:
public class PrincipalOrquesta {
public static void main (String args[]) {
Flauta miFlauta = new Flauta();
InstrumentoMusical instrumento = miFlauta;
instrumento.tocarInstrumento();
miFlauta.tocarinstrumento();
miFlauta.cambiarBoquilla();
}
}
Vemos la variación es que en la última línea del programa invocamos al nuevo método que dispone el objeto, pero únicamente lo podemos hacer desde su forma de Flauta, no desde su forma de InstrumentoMusical.
El Cast sirve para forzar el cambio de tipo de variable cuando sabemos que una variable puede adoptar la forma de un tipo inferior porque esa variable apunta a un objeto de ese tipo. El Cast en Java se realiza al asignar la variable a otra poniendo entre paréntesis el tipo al que queremos forzar la transformación. Para comprender este concepto vamos a implementar una nueva clase Piano que también implementa InstrumentoMusical.
public class Piano implements InstrumentoMusical {
public void tocar Instrumento() {
System.out.println("Suena el piano... pin pan pon");
}
}
Ahora modificamos la clase PrincipalOrquesta para realizar otro ejemplo haciendo Cast en nuestro programa:
public class void main (String args[]) {
InstrumentoMusical instrumento = new Piano();
instrumento.tocarInstrumento();
Piano miPiano = (Piano)instrumento;
}
}
Generamos una variable de tipo InstrumentoMusical y guardamos en ella un objeto de tipo Piano (porque un piano “es” un InstrumentoMusical) y ejecutamos su forma de instrumento, si luego queremos hacer la asignación a otra variable de tipo Piano, parece evidente que debería dejar hacerlo, pero es necesario realizar un Cast, que fuerza la conversión, porque una variable de tipo InstrumentoMusical puede albergar un objeto de tipo Flauta o de tipo Piano, por lo tanto se debe forzar el cambio, si no se escribe el Cast en el código no compilará.
El lenguaje permite hacer Cast a un tipo que no se corresponde con el tipo del objeto al que apunta la variable como es el caso del siguiente ejemplo. Para este caso, en tiempo de ejecución se lanzará una excepción de tipo ClassCastException. Pero el programa que escribimos a continuación compila perfectamente, aunque no funciona.
public class PrincipalOrquesta {
public static void main(String args[]){
InstrumentoMusical instrumento = new Piano();
instrumento.tocarInstrumento();
Flauta miFlauta = (Flauta)instrumento;
}
}
Para poder hacer verificaciones y conocer los tipos reales de los objetos, el lenguaje cuenta con un operador cuyo resultado de la evaluación devuelve true o false al comparar una referencia de un objeto con un tipo.
public class PrincipalOrquesta {
public static void main (String args[]) {
InstrumentoMusical = new Piano();
if (instrumento instanceof(Flauta)
System.out.println("Es una flauta");
if(instrumento instanceof(Piano)
System.out.println("Es un piano");
}
}
Si ejecutamos el programa veremos cómo el resultado es piano.
Modificadores en JAVA
Aunque ya los hemos visto en bastantes ocasiones, a continuación vamos a explicar el uso y las diferencias entre los diferentes modificadores de acceso que existen en el leguaje.
Java cuenta con cuatro modificadores de acceso diferentes, y es importante comprender su efecto en los campos y los métodos. Los modificadores son:
public: Un atributo, método o constructor public es accesible por cualquier otra clase.
private: Un atributo, método o constructor private es accesible únicamente desde dentro del ámbito de la clase. Es decir desde la llave de inicio después de la declaración de la clase hasta la llave de cierre de la clase.
protected: Un atributo, método o constructor protected es accesible desde las clases que se encuentran dentro del mismo paquete y por las clases que hereden de esta clase.
Sin modificador (o acceso por defecto): Un atributo, método o constructor con acceso por defecto sólo puede ser accedido desde clases que habiten en el mismo paquete.
package es.sb.ejemplo;
public class Vehiculo {
public int numeroRuedas;
int velocidadMaxima;
private String color;
public Vehiculo(int n, int e) {
numeroRuedas = n;
velocidadMaxima = e;
}
protected Vehiculo(int n, int e, String r) {
this(n, e);
color = r;
}
void modificarVelocidad(int nuevaVelocidad) {
System.out.println("Velocidad actual: " + nuevaVelocidad);
}
protected String getColor() {
return color;
}
}
La clase Vehiculo cuenta con las siguientes propiedades:
- La clase es public, por lo tanto se puede acceder desde cualquier otra.
- Su atributo numeroRuedas y su primer constructor son públicos, por lo que se puede acceder a ellos desde cualquier otra clase.
- Su atributo color es private, y por ello sólo se puede acceder a el desde la propia clase como se puede ver en el segundo constructor y en el método getColor.
- Su atributo velocidadMaxima tiene acceso por defecto y se puede acceder desde cualquier clase que programemos en el mismo paquete que esta clase: es.sb.ejemplo.
- El segundo constructor y el método getColor son protected y por lo tanto se podrá acceder a ellos desde cualquier clase que se encuentre dentro del paquete es.sb.ejemplo y desde cualquier clase que herede de Vehiculo.
Aparte de los modificadores de acceso en Java se cuenta con otros modificadores como abstract, que ya lo estudiamos en temas anteriores y está aplicado a la herencia.
Existe también el modificador final, que se aplica a las variables locales, los atributos, los métodos o las clases. Las consecuencias de utilizar este modificador son:
-
Una variable final no puede ser modificada. No referiremos a ella siempre como constantes.
-
Un método final no puede ser rescrito.
-
Una clase final no puede derivarse (no es posible extender de este tipo de clases).
Seguimos con el curso de Java en el siguiente enlace:
Programación en Java SE 6 desde cero: Parte 6
Las dudas que os puedan comentarlas justo aquí debajo e iremos respondiendo.
¡Deja un comentario!