Mostrando entradas con la etiqueta Java. Mostrar todas las entradas
Mostrando entradas con la etiqueta Java. Mostrar todas las entradas

Guia de colecciones en Java


Lo que me motivó a escribir esta guía de colecciones en java es el inmenso tamaño del framework de colecciones que puede echar para a atrás a cualquier programador que se ponga a investigar las APIs sin ordenar un poco las ideas. Hay tantas interfaces e implementaciones que es fácil perderse y no saber que usar en cada momento.

Esta pequeña guía de colecciones en Java pretende explicar, de manera sencilla, que las colecciones en Java y que implementación sería mejor que usases para cada caso.

Este va a ser un post extenso, así que voy a ser tan amable de proporcionaros una pequeña tabla de contenidos.

1- Tipos de colecciones en java
  • Listas
  • Sets
  • Maps
  • Colas

2- Listas
  • ArrayList
  • LinkedList
  • Vector
  • CopyOnWriteArrayList

3- Sets

  • HashSet
  • LinkedHashSet
  • TreeSet
  • EnumSet
  • CopyOnWriteArraySet
  • ConcurrentSkipListSet

4- Maps
  • HashMap
  • LinkedHashMap
  • TreeMap
  • EnumMap
  • WeakHashMap
  • HashTable
  • ConcurrentHashMap

5- Colas
  • ArrayDeque
  • LinkedBlockingDeque
  • LinkedList
  • PriorityQueue
  • PriorityBlockingQueue

6- Diagramas de clases
  • Diagrama de clases: Listas
  • Diagrama de clases: Sets
  • Diagrama de clases: Maps
  • Diagrama de clases: Colas

7- Notas finales


Tipos de colecciones en Java

Los tipos de colecciones vienen representados por interfaces. Una serie de interfaces clasifica las colecciones de Java en los siguientes tipos:

- Listas: Una lista ordenada, o secuencia. Normalmente permiten duplicados y tienen acceso aleatorio (es decir, puedes obtener elementos alojados en cualquier índice como si de un array se tratase).
- Sets: Colecciones que no admiten dos elementos iguales. Es decir, colecciones que no admiten que un nuevo elemento B pueda añadirse a una colección que tenga un elemento A cuando A.equals(B).
- Maps: Colecciones que asocian un valor a una clave. Parecido a la estructura de “array asociativo” que se encuentra en otros lenguajes. Un Map no puede tener dos claves iguales.
- Colas: Colecciones que permiten crear colas LIFO o FIFO. No permiten acceso aleatorio, solo pueden tomarse objetos de su principio, final o ambos, dependiendo de la implementación.

Listas

La colección más básica de Java. También, es la más usada por los programadores que no han investigado el framework de colecciones a fondo por hacernos pensar que se trata de una especie de array hipervitaminado ya que hace su trabajo y es fácil de entender.

Sin embargo, por grande que sea la tentación de usar una lista (como un ArrayList) para todo, ¡has de resistir!

Sabiendo los requisitos de tu aplicación, seguramente encontrarás otra colección que haga el trabajo de una lista mejor. Procura asegurarte de que cuando vas a usar una lista es porque realmente te hace falta, no porque no conoces el resto. La diferencia de rendimiento puede ser enorme. Y cuando digo enorme, me refiero a superior a un 600% en según que operaciones.

¿Qué beneficios tienen las listas?

- Acceso aleatorio.
- Están ordenadas (Podemos usar Colections.sort() para ordenar los elementos siguiendo el criterio que queramos).
- Podemos añadir / eliminar elementos sin ninguna restricción.
- Tienen un iterador especial ListIterator que permite modificar la lista en cualquier dirección.
- Siguen la notación de los arrays, por lo que son fáciles de comprender.

¿Qué problemas tienen las listas?

- Bajo rendimiento en operaciones especialziadas respecto a otras colecciones.

A continuación voy a enumerar todas las listas de Java con sus características, además, daré una pequeña explicación de cuando deberíais usar esa lista.

Tipos de Lista
  • ArrayList: Muy rápida accediendo a elementos, relativamente rápida agregando elementos si su capacidad inicial y de crecimiento están bien configuradas. Es la lista que deberías usar casi siempre.
  • LinkedList: Una lista que también es una cola (hablaré de esto más tarde). Más rápida que ArrayList añadiendo elementos en su principio y eliminando elementos en general. Utilízala en lugar de ArrayList si realizas más operaciones de inserción (en posición 0) o de eliminación que de lectura. La diferencia de rendimiento es enorme.
  • Vector: Terreno peligroso. Vector es una colección deprecated (obsoleta), así que usadla únicamente si necesitáis un ArrayList concurrente. El rendimiento de Vector es superior al de Collections.syncronizedList(new ArrayList()).
  • CopyOnWriteArrayList: Colección concurrente que es muy poco eficiente en operaciones de escritura, pero muy rápida en operaciones de lectura. Usala sobre Vector (o synced ArrayList) cuando el número de lecturas concurrentes sea mucho mayor al número de escrituras.

Sets

Los sets, o conjuntos, son colecciones que por norma general no admiten elementos iguales en su interior. Como mencionaba antes, dos elementos A y B son iguales si A.equals(B).

Podemos añadir y eliminar elementos de un set, así como saber si determinado elemento está en un set, pero no tenemos acceso aleatorio, por lo que hay que tener muy claro cuando queremos usarlos.

Una colección normal, en apariencia, nos da todo lo anterior -excepto el hecho de eliminar duplicados-, así que, ¿por qué usar un Set y no una Lista?, el motivo es simple: eficiencia.

Supongamos este código:

Código: Java
  1. List b = new ArrayList();
  2.      
  3. if (!b.contains(1))
  4.     b.add(1);

La funcionalidad es la misma que la de set.add, pero el rendimiento es muchísimo peor, hasta el punto de que podemos tener problemas muy serios cuando añadamos muchos elementos y no queremos eso, ¿verdad?.

¿Qué beneficios tienen los sets?

- No permiten elementos duplicados.
- Implementación muy eficiente de .add para asegurarnos de que no hay duplicados.

¿Qué desventajas tienen?

- No tienen acceso aleatorio.
- Solo algunos tipos de set pueden ordenarse y lo hacen de forma poco eficiente.

¿Cuando deberíamos usar un set sobre una lista?, y ¿qué tipo de set?, veámoslo…

Tipos de Set

  • HashSet: La implementación más equilibrada de la interfaz Set. Es rápida y no permite duplicados, pero no tiene ningún tipo de ordenación. Utilízala si necesitas un control de duplicados pero no ningún tipo de ordenación o acceso aleatorio.
  • LinkedHashSet: Un HashSet que incluye ordenación de elementos por orden de entrada, una velocidad de iteración mucho mayor y un rendimiento mucho peor a la hora de añadir elementos. Utilizala si necesitas un Set ordenado por orden de inserción o si vas a usar un Set que vaya a realizar solamente operaciones de iteración.
  • TreeSet: Un set que incluye una implementación de un árbol rojo-negro. Este Set puede ser ordenado, pero su rendimiento es mucho peor en cualquier operación (menos iteración) respecto aun HashSet. Utilizalo solo si necesitas un Set con un criterio de ordenación específico y ten cuidado con las inserciones.
  • EnumSet: La mejor implementación de Set para tipos enumerados (Enum). Utilizala si quieres crear un Set de Enums.
  • CopyOnWriteArraySet: Set concurrente que tiene un gran rendimiento de lectura, pero pésimo de escritura, eliminado y contains. Úsalo solo en Sets concurrentes que apenas tengan estas operaciones.
  • ConcurrentSkipListSet: Set concurrente y ordenable. Utilizalo solo cuando requieras un Set ordenable (como TreeSet) en entornos de concurrencia. En Sets de tamaños muy grandes su rendimiento empeora notablemente.

Maps

Los maps son colecciones que asocian un valor con una clave. Tanto la clave como el valor pueden ser cualquier tipo de datos de Java: Objetos, primitivos, otras colecciones, etc.

Las implementaciones son muy parecidas a los Sets debido a que, internamente, utilizan un Set (la implementación varía según el tipo de Map) para garantizar que no hay elementos duplicados en las claves.

¿Que ventajas tienen los map?

- Asociación clave -> valor.
- Gracias a que utilizan internamente un Set, garantizan que no habrá dos claves iguales.
- Es fácil reconocer cuando necesitamos usar un Map.

¿Qué desventajas tienen los map?

- Rendimiento no muy elevado comparado con otras colecciones.

Tipos de Map
  • HashMap: La implementación más genérica de Map. Un array clave->valor que no garantiza el orden de las claves (de la misma forma que un HashSet). Si necesitas un Map no-concurrente que no requiera ordenación de claves, este es el tuyo. Si os fijais en el código de HashSet, veréis que curiosamente utiliza un HashMap internamente.
  • LinkedHashMap: Implementación de map que garantiza orden de claves por su tiempo de inserción; es decir, las claves que primero se creen serán las primeras. También puede configurarse para que la orden de claves sea por tiempo de acceso (las claves que sean accedidas precederán a las que no son usadas). Itera más rápido que un HashMap, pero inserta y elimina mucho peor. Utilizalo cuando necesites un HashMap ordenado por orden de inserción de clave.
  • TreeMap: Un Map que permite que sus claves puedan ser ordenadas, de la misma forma que TreeSet. Usalo en lugar de un HashMap solo si necesitas esta ordenación, ya que su rendimiento es mucho peor que el de HashMap en casi todas las operaciones (excepto iteración).
  • EnumMap: Un Map de alto rendimiento cuyas claves son Enumeraciones (Enum). Muy similar a EnumSet. Usadlo si vais a usar Enums como claves.
  • WeakHashMap: Un Map que solo guarda referencias blandas de sus claves y valores. Las referencias blandas hacen que cualquier clave o valor sea eligible por el recolector de basura si no hay ninguna otra referencia al mismo desde fuera del WeakHashMap. Usa este Map si quieres usar esta característica, ya que el resto de operaciones tienen un rendimiento pésimo. Comúnmente usado para crear registros que vayan borrando propiedades a medida que el sistema no las vaya necesitando y vaya borrando sus referencias.
  • HashTable: Map deprecated y concurrente. Básicamente, es un HashMap concurrente que no debes usar nunca. En su lugar, utiliza ConcurrentHashMap.
  • ConcurrentHashMap: Un Map concurrente que no permite valores nulos. Sustitución básica de HashTable. Usala si necesitas un HashMap concurrente.

Colas

Las colas son estructuras que ofrecen un gran rendimiento al obtener elementos de su principio o de su final, representando colas LIFO / FIFO, aunque también veremos colas ordenadas en función de otros criterios.

Deberás usar una cola cuando vayas a recuperar siempre el primer o último elemento de una serie. Se usan para implementar las mencionadas colas LIFO / FIFO, así como colas de prioridades (como puede ser un sistema de tareas o de procesos).

Cabe destacar que hay dos tipos de colas, Queues y Deques. Las primeras solo proporcionan métodos para acceder al último elemento de la cola, mientras que las Deques permiten acceder a cualquiera de los dos extremos.

¿Qué ventajas tienen las colas?

- Ofrecen un gran rendimiento al recuperar el primer o último objeto de la cola.
- Permiten crear estructuras LIFO / FIFO o colas de prioridades con muy buen rendimiento.

¿Qué desventajas tienen las colas?

- La iteración por las colas suele ser muy lenta.
- El acceso aleatorio, de la misma manera, es muy lento.

Tipos de colas
  • ArrayDeque: Una implementación de Deque de rendimiento excepcional. Implementa tanto cola LIFO como FIFO al ser una Deque y es la Cola que deberías usar si quieres implementar una de estas dos estructuras.
  • LinkedBlockingDeque: Una Deque concurrente que has de usar cuando quieras usar un ArrayDeque en entornos multihilo.
  • LinkedList: LinkedList, anteriormente mencionada en la sección de listas, también es una Deque, sin embargo, su rendimiento es muy inferior al de ArrayDeque. No deberías usar LinkedList cuando quieras usar una cola.
  • PriorityQueue: Una cola que se ordena mediante un Comparator, permitiendo crear una Cola donde el primer elemento no dependerá de su tiempo de inserción, sino de cualquier otro factor (tamaño, prioridad, etc). Deberemos usarlo cuando necesitemos este comparator, ya que ArrayDeque no lo permite.
  • PriorityBlockingQueue: La versión concurrente de PriorityQueue.

Diagramas de clases

Para entender mejor el framework de colecciones, nada como un diagrama de clases de todas los tipos de colección. He aquí dicho diagrama, por cortesía de karambelkar.info:

Diagrama de clases – List


Diagrama de clases – Set


Diagrama de clases – Maps


Diagrama de clases – Colas


Notas finales
A la hora de usar colecciones en Java, lo más importante es elegir el tipo de colección. Cuando trabajemos con colecciones, siempre declararemos las instancias con el nombre de su interfaz, aprovechando el enlace dinamico.

De esta forma, aunque la implementación cambie en el futuro, podemos tener claro que la aplicación no va a dar fallos (ya que estamos usando las interfaces y estas tienen los métodos que usan sus implementaciones).

A la hora de escoger la implementación de cada tipo de colección, debéis fijaros en dos cosas:

- Si estamos trabajando en un entorno concurrente o no.
- En el rendimiento de las operaciones que vamos a necesitar en las distintas implementaciones válidas para nuestro entornos (concurrentes o no).

Sabiendo eso ya podréis empezar a explotar a fondo el Framework de colecciones en Java y no limitaros a usar las colecciones Vanilla, mal de muchos programadores que empiezan en este lenguaje.

Confío en que la lectura haya sido interesante y fructífera, para mi lo ha sido, por lo menos.

Fuente: luaces-novo

[Java] Buenas prácticas en programación



1.- Evitar la creación innecesaria de objetos, Lazy Initialitation

La creación de objetos en Java es una de las operaciones mas costosas en términos de uso de memoria e impacto en el performance.  Esto es evitable creando o inicializando objetos solo en el momento en que serán requeridos en el código.

Código: Java
  1. public class Paises {
  2.  
  3.     private List paises;
  4.  
  5.     public List getPaises() {
  6.         //se inicializa solo cuando es requerido
  7.         if(null == paises) {
  8.             paises = new ArrayList();
  9.         }
  10.         return paises;
  11.     }
  12. }


2.- Nunca hacer variables de instancia públicas

Hacer una clase publica se puede ocasionar problemas en un programa.  Por ejemplo si tienes una clase MiCalendario. Esta clase contiene un arreglo de cadenas diasDeLaSemana.  Pero es una arreglo público y este puede ser accedido por cualquiera.  Tu asumes que este arreglo contiene siempre los 7 nombres de los días de la semana.  Alguien por error puede cambiar el valor e insertar un error!

Código: Java
  1. public class MiCalendario {
  2.    
  3.     public String[] diasDeLaSemana =
  4.         {"Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"};
  5.    
  6.     //mas código
  7.    
  8. }


La mejor práctica es como mucho de ustedes saben, es siempre definir estas variables como privadas y crear los métodos accesores, “setters“ y “getters”

Código: Java
  1. private String[] diasDeLaSemana =
  2.     {"Domingo", "Lunes", "Martes", "Miercoles", "Jueves", "Sabado", "Domingo"};
  3.  
  4. public String[] getDiasDeLaSemana() {
  5.     return diasDeLaSemana;
  6. }


Pero escribir los métodos accesores no resuelve el problema del todo.  El arreglo sigue siendo accesible.  La mejor forma de hacerlo inmodificable es devolviendo un arreglo clonado en lugar del arreglo mismo.  Esto se logra modificando el método get de la siguiente forma.

Código: Java
  1. public String[] getDiasDeLaSemana() {
  2.     return diasDeLaSemana.clone();
  3. }


3.- Tratar siempre de minimizar la Mutabilidad de las clases

Hacer una clase inmutable es hacerla inmodificable.   La información de la clase se preservara durante el tiempo de vida de la clase.  Las clases inmutables son simples y fáciles de manejar.  Son “thread safe”.  Normalmente son los bloques para formar otros objetos mas grandes.

No obstante, crear objetos inmutables pueden golpear significativamente el rendimiento de una aplicación.  Así que elije cuidadosamente si quieres que una clase sea o no inmutable.  Trata siempre de tener clases pequeñas con el menor número de clases inmutables.

Para hacer una clase inmutable puedes definir sus constructor de forma privada y luego crear un método estático para inicializar al objeto y devolverlo.

Código: Java
  1. public class Empleado {
  2.  
  3.     private String primerNombre;
  4.     private String segundoNombre;
  5.    
  6.     // constructor private default
  7.     private Empleado (String primerNombre, String segundoNombre) {
  8.         this. primerNombre = primerNombre;
  9.         this. segundoNombre = segundoNombre;
  10.     }
  11.    
  12.     public static Empleado valueOf (String primerNombre, String segundoNombre) {
  13.         return new Empleado (primerNombre, segundoNombre);
  14.     }
  15. }


4.- Trata de usar mas las Interfaces sobre las Clases Abstractas

No es posible la herencia múltiple en Java, pero definitivamente puedes implementar múltiples interfaces.  Esto hace que cambiar la implementación de una clase existente sea fácil y que puedas implementar una o mas interfaces en lugar de cambiar la jerarquía completa de clases.

Pero si tu estás cien por ciento seguro de que métodos de una interface tendrás, entonces solo implementa esa interfaz.   Es bastante difícil agregar un nuevo método en una interfaz existente sin alterar todo el código que se está implementando.  Por el contrario un nuevo método puede ser fácilmente agregado en una clase abstracta sin alterar la funcionalidad existente.

5.- Limita siempre el alcance de una variable local

Las variables locales son grandiosas.  Pero algunas veces pueden insertar mas bugs durante el copiado y pegado de código viejo. Minimizar el alcance de una variable local hace que el código sea mas legible, menos propenso a errores  y mas mantenible.

Por lo tanto, debemos declarar variables justo antes de ser usadas.

Procura inicializar una variable desde su declaración.  Si eso no es posible asígnale el valor nulo.

6- Trata de usar librerías estándar en lugar de hacer las tuyas desde cero.

Escribir código es divertido.  Pero no reinventes la rueda.  Es bastante recomendable usar librerías estándar que ya han sido probadas, debugeadas y usadas por otros.    Esto no solo mejora la eficiencia de un programador sino que redice las posibilidades de tener errores en el código.  Además, usar una librería estándar  hace al código mas legible y mantenible.

Por ejemplo Google tiene liberada la nueva librería Google Collections que puede ser usada para agregar mas funcionalidad a tu código.

7.- Siempre que sea posible trata de usar tipos primitivos en lugar de las clases Wrapper

Las clases Wrapper son buenas, pero también son lentas.  Los tipos primitivos son como clases, sin embargo las clases wrapper almacenan la información completa acerca de una clase.

Algunas veces un programador puede agregar un error en el código usando una wrapper por un descuido.  Por ejemplo:

Código: Java
  1. int x = 10;
  2. int y = 10;
  3.  
  4. Integer x1 = new Integer(10);
  5. Integer y1 = new Integer(10);
  6.  
  7. System.out.println(x == y);
  8. System.out.println(x1 == y1);


El primer System.out.println imprimirá true mientras que el segundo imprimirá false.  El problema cuando comparas dos clases wrapper es que no se puede usar el operador ==, por que en realidad se están comparando referencias y no sus valores actuales.

Además si estás usando una clase wrapper no debes olvidar inicializarla.  Porque el valor por default de las variables wrapper es null.

Código: Java
  1. Boolean bandera = null;
  2.      
  3.         //mas código
  4.  
  5. if(flag == true) {
  6.     System.out.println("Se establece el valor de bandera ");
  7. } else {
  8.     System.out.println("No se establece el valor de bandera");
  9. }


El código lanzará un NullPointerException cuando se trate de comparar con true y el valor sea nulo si en el código intermedio no fue inicializada.

8.- Usa los Strings con mucho cuidado.

Usa siempre las cadenas con mucho cuidado en tu código.  Una simple concatenación de cadenas puede reducir el rendimiento de tu programa.  Por ejemplo si queremos concatenar cadenas usando el operador “+” en un ciclo for entonces todo el tiempo se estará creando un objeto String.  Esto afecta tanto a la memoria como al rendimiento.

Además en lugar de que instancies una objeto String no uses su constructor, sino que debes instanciarlo directamente. Por ejemplo:

Código: Java
  1. //instanciación lenta
  2. String lenta = new String("solo otro objeto string");
  3.  
  4. //instanciación rápida
  5. String rapida = "solo otro objeto string";


9.- Siempre regresa colecciones vacías en lugar de nulas

No importa que tu método regrese una colección o un arreglo, siempre asegúrate de que cuando sea necesario se regrese vacío y no nulo, en aquellos casos en los que no contendrá elementos porque la lógica de tu programa lo requiera.  Esto te ahorrará un montón de tiempo cuando hagas pruebas para valores nulos.

10.- El copiado defensivo es salvador

El copiado defensivo hace que los objetos creados estén libres de la mutación.  Por ejemplo en el código siguiente tenemos definida la clase Estudiante la cual a su vez tiene una variable con la fecha de nacimiento que es inicializada cuando el objeto es construido.

Código: Java
  1. public class Estudiante {
  2.     private Date fechaNacimiento;
  3.    
  4.     public Estudiante(fechaNacimiento) {
  5.         this. fechaNacimiento = fechaNacimiento;
  6.     }
  7.    
  8.     public Date getFechaNacimiento () {
  9.         return this.fechaNacimiento;
  10.     }
  11. }


Ahora podríamos tener el siguiente código que use al objeto Estudiante.

Código: Java
  1. public static void main(String []arg) {
  2.     Date fechaNacimiento = new Date();    
  3.     Estudiante estudiante = new Student(fechaNacimiento);
  4.     fechaNacimiento.setYear(2019);
  5.     System.out.println(estudiante.getFechaNacimiento ());
  6. }


En el código siguiente creamos tan solo al objeto Estudiante con algunas fechas de nacimiento por default.  Pero entonces cambiamos el valor de el año de nacimiento.  Después imprimimos el año de nacimiento, este año fue cambiado por 2019!

Para evitar estos casos, se puede utilizar el mecanismo defensivo copias. Cambie el constructor de la clase del estudiante a lo siguiente.

Código: Java
  1. public Estudiante(fechaNacimiento) {
  2.     this.fechaNacimiento = new Date(fechaNacimiento);
  3. }


Esto para asegurarnos de tener otra copia de la fecha de nacimiento que usamos en clase Estudiante.

11.-  Nunca dejes salir una excepción de un bloque finally

12.- Nunca lances "Exception" directamente.


Extraido de: viralpatel.net

Patrones de diseño

 
PATRONES DE DISEÑO


CAPÍTULO I

Introducción

Los patrones de diseño son la base para la búsqueda de soluciones a problemas comunes en el desarrollo de software y otros ámbitos referentes al diseño de interacción o interfaces.
Un patrón de diseño resulta ser una solución a un problema de diseño. Para que una solución sea considerada un patrón debe poseer ciertas características. Una de ellas es que debe haber comprobado su efectividad resolviendo problemas similares en ocasiones anteriores. Otra es que debe ser reutilizable, lo que significa que es aplicable a diferentes problemas de diseño en distintas circunstancias.

Los patrones de diseño pretenden:

  • Proporcionar catálogos de elementos reusables en el diseño de sistemas software.
  • Evitar la reiteración en la búsqueda de soluciones a problemas ya conocidos y solucionados anteriormente.
  • Formalizar un vocabulario común entre diseñadores.
  • Estandarizar el modo en que se realiza el diseño.
  • Facilitar el aprendizaje de las nuevas generaciones de diseñadores condensando conocimiento ya existente.


Asimismo, no pretenden:

  • Imponer ciertas alternativas de diseño frente a otras.
  • Eliminar la creatividad inherente al proceso de diseño.


No es obligatorio utilizar los patrones, solo es aconsejable en el caso de tener el mismo problema o similar que soluciona el patrón, siempre teniendo en cuenta que en un caso particular puede no ser aplicable. "Abusar o forzar el uso de los patrones puede ser un error".


PATRÓN ADAPTER


El patrón adapter nos permite ampliar la funcionalidad de una clase o interfaz adaptando objetos que no coinciden con una determinada jerarquía de clases.

Convierte la interfaz de una clase en otra interfaz que el cliente espera. Adapter permite a las clases trabajar juntas, lo que de otra manera no podría hacerlo debido a sus interfaces incompatibles.

Cuándo usarlo:

  • Se desea usar una clase existente, y su interfaz no se iguala con la necesitada.
  • Cuando se desea crear una clase reusable que coopera con clases no relacionadas, es decir, las clases no tienen necesariamente interfaces compatibles.
Problema: Se nos pide adaptar un auto eléctrico a un sistema de abastecimiento de combustible para autos. Siendo que el auto eléctrico, utiliza como combustible la energía eléctrica.

Solución: Utilizar el patrón Adapter para extender la funcionalidad de la interfaz adaptando el auto eléctrico.

Creamos una interfaz llamada Car que representará de forma genérica a un Auto:

Car.java
Código: Java
  1. package org.underc0de.adapter;
  2.  
  3. public interface Car {
  4.  
  5.         void fillTank();
  6.         default void start() {
  7.                 System.out.println("Encendiendo auto...");
  8.         }
  9. }

NOTA: el método start() lo predefinimos porque será el mismo para todos las implementaciones.

Ahora sus implementaciones: GasolineCar y GasCar. Representan un auto a gasolina y otro a gas.

GasolineCar.java
Código: Java
  1. package org.underc0de.adapter;
  2.  
  3. public class GasolineCar implements Car {
  4.  
  5.         public GasolineCar() {
  6.                 super(); // llamada al constructor padre
  7.                 System.out.println("Creando un auto a gasolina...");
  8.         }
  9.        
  10.         @Override
  11.         public void fillTank() {
  12.                 System.out.println("Colocando gasolina...");
  13.         }
  14. }
  15.  

GasCar.java
Código: Java
  1. package org.underc0de.adapter;
  2.  
  3. public class GasCar implements Car {
  4.  
  5.         public GasCar() {
  6.                 super(); // llamada al constructor padre
  7.                 System.out.println("Creando un auto a gas...");
  8.         }
  9.        
  10.         @Override
  11.         public void fillTank() {
  12.                 System.out.println("Colocando gas...");
  13.         }
  14.  
  15. }

Ambos autos, GasolineCar y GasCar necesitan combustible para funcionar. Por eso, el método fillTank llena el combustible de ellos, dependiendo si es gasolina o gas.

Ahora queremos añadir un auto eléctrico a nuestra jerarquía de clases.

ElectricCar.java
Código: Java
  1. package org.underc0de.adapter;
  2.  
  3. public class ElectricCar {
  4.  
  5.         public ElectricCar() {
  6.                 super(); // llamada al constructor padre
  7.                 System.out.println("Creando un auto eléctrico...");
  8.         }
  9.        
  10.         public void connect() {
  11.                 System.out.println("Conectando motor a generador de electricidad...");
  12.         }
  13. }

Nos damos con la sorpresa que éste auto no coincide con nuestra jerarquía. La interfaz Car dice que todos los autos que la implementen deben tener el método fillTank. Pero, ¿Como un auto eléctrico puede llenar el tanque? ¿Cómo hacemos para adaptar éste auto a nuestra jerarquía?

Un error es común es modificar la interfaz o clase abstracta. Ésto viola el principio OCP, el cual nos dice:
Citar
Las entidades de software deben ser abiertas para ser extendidas y cerradas para no ser modificadas.

Aquí toma importancia en patrón Adapter. Éste patrón nos permite ampliar la funcionalidad de una interfaz si tener que cambiar código en ella. Veamos como funciona.

Código: Java
  1. package org.underc0de.adapter;
  2.  
  3. public class ElectricCarAdapter implements Car {
  4.  
  5.         ElectricCar electricCar;
  6.        
  7.         public ElectricCarAdapter() {
  8.                 electricCar = new ElectricCar();
  9.         }
  10.        
  11.         @Override
  12.         public void fillTank() {
  13.                 electricCar.connect();
  14.                
  15.         }
  16.  
  17. }

Como vemos hemos podido adaptar nuestro auto eléctrico a nuestra interfaz Car. ¡Ahora, podemos crear tanto autos a gasolina, gas o eléctricos aplicando polimorfismo!

Veamos que salida nos arroja:

AdapterTest.java
Código: Java
  1. package org.underc0de.adapter;
  2.  
  3. public class AdapterTest {
  4.  
  5.         public static void main(String[] args) {
  6.                 Car gasolineCar = new GasolineCar();
  7.                 gasolineCar.fillTank();
  8.                 gasolineCar.start();
  9.                
  10.                 System.out.println();
  11.                
  12.                 Car gasCar = new GasCar();
  13.                 gasCar.fillTank();
  14.                 gasCar.start();
  15.                
  16.                 System.out.println();
  17.                
  18.                 Car electricCar = new ElectricCarAdapter();
  19.                 electricCar.fillTank();
  20.                 electricCar.start();
  21.         }
  22.  
  23. }

Salida:

Código: [Seleccionar]
Creando un auto a gasolina...
Colocando gasolina...
Encendiendo auto...

Creando un auto a gas...
Colocando gas...
Encendiendo auto...

Creando un auto eléctrico...
Conectando motor a generador de electricidad...
Encendiendo auto...

Y así podemos extender la funcionalidad de nuestra aplicación de forma sencilla y eficiente.


PATRÓN FACADE


Descripción: El patrón fachada viene motivado por la necesidad de estructurar un entorno de programación y reducir su complejidad con la división en subsistemas, minimizando las comunicaciones y dependencias entre éstos.

Cuándo usarlo:

  • Se usa para proporcionar una interfaz sencilla para un sistema complejo.
  • Se quiere desacoplar un subsistema de sus clientes u otros subsistemas, haciéndolo mas independiente y portable.
  • Se quiera dividir los sistemas en niveles: las fachadas serian el punto de entrada a cada nivel.
Pros/contras:

  • + La principal ventaja del patrón fachada consiste en que para modificar las clases de los subsistemas, sólo hay que realizar cambios en la interfaz/fachada, y los clientes pueden permanecer ajenos a ello. Además, y como se mencionó anteriormente, los clientes no necesitan conocer las clases que hay tras dicha interfaz.
  • - Como inconveniente, si se considera el caso de que varios clientes necesiten acceder a subconjuntos diferentes de la funcionalidad que provee el sistema, podrían acabar usando sólo una pequeña parte de la fachada, por lo que sería conveniente utilizar varias fachadas más específicas en lugar de una única global.
Problema: Crear una aplicación que haga tres operaciones bancarias: Crear una cuenta, depositar dinero y retirar dinero. Las operaciones deben hacerse dentro de una sola entidad que las maneje.

Solución: Aplicar el patrón Facade para encapsular todos los objetos que hacen las 3 operaciones.

Bank.java
Código: Java
  1. package org.underc0de.facade;
  2.  
  3. public class Bank {
  4.  
  5.         public Bank() {
  6.                
  7.         }
  8.        
  9.         public void createAccount(String account) {
  10.                 System.out.println("Creando cuenta N° "+account);
  11.         }
  12. }

Deposit.java
Código: Java
  1. package org.underc0de.facade;
  2.  
  3. public class Deposit {
  4.  
  5.         public Deposit() {
  6.                
  7.         }
  8.        
  9.         public void makeDeposit(double amount, String account) {
  10.                 System.out.println("Se ha depositado $"+amount+" a la cuenta "+account);
  11.         }
  12.        
  13. }

Withdrawal.java
Código: Java
  1. package org.underc0de.facade;
  2.  
  3. public class Withdrawal {
  4.  
  5.         public Withdrawal() {
  6.                
  7.         }
  8.        
  9.         public void makeWidthdrawal(double amount, String account) {
  10.                 System.out.println("Se ha retirado $"+amount+" de la cuenta "+account);
  11.         }
  12.        
  13. }

Ahora, creamos nuestra clase principal:

FacadeTest.java
Código: Java
  1. package org.underc0de.facade;
  2.  
  3. public class Facade {
  4.  
  5. public static void main(String[] args) {
  6.  
  7.         Bank bank = new Bank();
  8.         Deposit deposit = new Deposit();
  9.         Withdrawal withdrawal = new Withdrawal();
  10.                
  11.         bank.createAccount("9343435093");
  12.         deposit.makeDeposit(2599.90, "9343435093");
  13.         withdrawal.makeWidthdrawal(699.90, "9343435093");
  14.                        
  15.         }
  16.  
  17. }

Como vemos creamos 3 objetos los cuales se encargan de efectuar las acciones. Pero, ¿es necesario crear éstos tres objetos en éste ambito? ¿Los vamos a necesitar siempre?

Una mejor idea sería encapsular éstos objetos dentro de uno solo que se encargue de realizar todas las operaciones. Éste es el propósito del patrón Facade, actuar como intermediario entre la interfaz y las funcionalidades de un sistema. Veamos como se representa:

OperationsFacade.java
Código: Java
  1. package org.underc0de.facade;
  2.  
  3. public class OperationsFacade {
  4.  
  5.        
  6.         public OperationsFacade() {
  7.         }
  8.        
  9.         public void createAccount(String account) {
  10.                 new Bank().createAccount(account);
  11.         }
  12.        
  13.         public void makeDeposit(double amount, String account) {
  14.                 new Deposit().makeDeposit(amount, account);
  15.         }
  16.        
  17.         public void makeWithdrawal(double amount, String account) {
  18.                 new Withdrawal().makeWidthdrawal(amount, account);
  19.         }
  20.        
  21. }

Como podemos observar, ésta clase encapsula el comportamiento de las clases Bank, Deposit y Withdrawal. Ya no tenemos que declarar los objetos porque éstos se crean en los métodos de OperationsFacade y se destruyen al finalizar el mismo. ¡Además ahorramos memoria!

Ahora, veamos como queda la clase principal:

FacadeTest.java
Código: Java
  1. package org.underc0de.facade;
  2.  
  3. public class FacadeTest {
  4.  
  5.         public static void main(String[] args) {
  6.                 OperationsFacade facade = new OperationsFacade();
  7.                 facade.createAccount("9343435093");
  8.                 facade.makeDeposit(2599.90, "9343435093");
  9.                 facade.makeWithdrawal(699.90, "9343435093");
  10.         }
  11.  
  12. }

Como podemos ver, solo necesitamos de un objeto OperationsFacade para realizar todas las operaciones. Así aplicamos también el principio KISS (Keep it simple stupid!).

Salida:

Código: [Seleccionar]
Creando cuenta N° 9343435093
Se ha depositado $2599.9 a la cuenta 9343435093
Se ha retirado $699.9 de la cuenta 9343435093


PATRÓN ABSTRACT FACTORY


Descripción: El patrón Abstract Factory nos permite crear, mediante una interfaz, conjuntos o familias de objetos (denominados productos) que dependen mutuamuente y todo esto sin especificar cual es el objeto concreto.

Cuándo usarlo:

  • Un sistema debe ser independiente de como sus objetos son creados.
  • Un sistema debe ser 'configurado' con una cierta familia de productos.
  • Se necesita reforzar la noción de dependencia mutua entre ciertos objetos.
Pros/contras:

  • + Brinda flexibilidad al aislar a las clases concretas.
  • + Facilita cambiar las familias de productos.
  • - Para agregar nuevos productos se deben modificar tanto las fabricas abstractas como las concretas.
Problema: Se nos pide crear animales de X tipo sin especificar cuál es el objeto en concreto.

Solución: Utilizar el patrón Abstract Factory para crear los objetos solicitados.

Primero, creamos una clase abstracta llamada Animal que representará de forma genérica a un Animal:

Animal.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public abstract class Animal {
  4.  
  5.         protected String type;
  6.         protected String family;
  7.         protected String habitat;
  8.        
  9.         public Animal(String type, String family, String habitat) {
  10.                 this.type = type;
  11.                 this.family = family;
  12.                 this.habitat = habitat;
  13.         }
  14.        
  15.         public String getType() {
  16.                 return type;
  17.         }
  18.        
  19.         public void setType(String type) {
  20.                 this.type = type;
  21.         }
  22.  
  23.         public String getFamily() {
  24.                 return family;
  25.         }
  26.  
  27.         public void setFamily(String family) {
  28.                 this.family = family;
  29.         }
  30.  
  31.         public String getHabitat() {
  32.                 return habitat;
  33.         }
  34.  
  35.         public void setHabitat(String habitat) {
  36.                 this.habitat = habitat;
  37.         }
  38.  
  39.         @Override
  40.         public String toString() {
  41.                 return "Tipo de animal: "+getType()+"\nFamilia: "+getFamily()+
  42.                                 "\nHábitat: "+getHabitat();
  43.         }
  44. }
  45.  

Y tres animales que extienden de Animal:

Dog.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class Dog extends Animal {
  4.  
  5.         public Dog(String type, String family, String habitat) {
  6.                 super(type, family, habitat);
  7.         }
  8.  
  9. }

Shark.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class Shark extends Animal {
  4.  
  5.         public Shark(String type, String family, String habitat) {
  6.                 super(type, family, habitat);
  7.         }
  8.  
  9.  
  10. }

Lion.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class Lion extends Animal {
  4.  
  5.         public Lion(String type, String family, String habitat) {
  6.                 super(type, family, habitat);
  7.         }
  8.  
  9.  
  10. }

Bien, ya tenemos nuestros 3 tipos de animales que heredan de Animal: Dog, Shark y Lion. Como la tarea es construir objetos sin especificar de qué tipo son, creamos una fábrica abstracta que especifica la creación de un Animal genérico sin especificar de qué tipo:

AbstractAnimalFactory.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public interface AbstractAnimalFactory {
  4.  
  5.         Animal createAnimal();
  6. }

Ahora, el siguiente paso es hacer las implementaciones de ésta fábrica abstracta, es decir las fábricas concretas que crearán los objetos concretos:

DogFactory.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class DogFactory implements AbstractAnimalFactory {
  4.  
  5.         protected String type;
  6.         protected String family;
  7.         protected String habitat;
  8.        
  9.         public DogFactory(String type, String family, String habitat) {
  10.                 this.type = type;
  11.                 this.family = family;
  12.                 this.habitat = habitat;
  13.         }
  14.        
  15.         @Override
  16.         public Animal createAnimal() {
  17.                 return new Dog(type, family, habitat);
  18.         }
  19.  
  20. }

SharkFactory.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class SharkFactory implements AbstractAnimalFactory {
  4.  
  5.         protected String type;
  6.         protected String family;
  7.         protected String habitat;
  8.        
  9.         public SharkFactory(String type, String family, String habitat) {
  10.                 this.type = type;
  11.                 this.family = family;
  12.                 this.habitat = habitat;
  13.         }
  14.        
  15.         @Override
  16.         public Animal createAnimal() {
  17.                 return new Shark(type, family, habitat);
  18.         }
  19.  
  20. }

LionFactory.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class LionFactory implements AbstractAnimalFactory {
  4.  
  5.         protected String type;
  6.         protected String family;
  7.         protected String habitat;
  8.        
  9.         public LionFactory(String type, String family, String habitat) {
  10.                 this.type = type;
  11.                 this.family = family;
  12.                 this.habitat = habitat;
  13.         }
  14.        
  15.         @Override
  16.         public Animal createAnimal() {
  17.                 return new Lion(type, family, habitat);
  18.         }
  19.  
  20. }

Analicemos un poco el código. Cada una de las implementaciones de AbstractAnimalFactory especifican la implementación que tendrá para crear un tipo de animal.

Cada factoría concreta, tiene las 3 propiedades que necesita cada objeto para poder crearse. Así mismo, dichos datos se pasan como parámetros a su constructor para establecer los valores en las propiedades que se utilizarán para crear una instancia del objeto.

Establece las propiedades:

Código: Java
  1. public LionFactory(String type, String family, String habitat)

Y usa esas mismas propiedades para crear el objeto:

Código: Java
  1. @Override
  2. public Animal createAnimal() {
  3.         return new Lion(type, family, habitat);
  4. }

Finlamente, se sobre-escribe el método de la factoría abstracta createAnimal() para retornar un nuevo objeto de acuerdo al tipo de factoría. Así, la factoría de Perros, creará objetos tipo Perro, la factoría de Tiburones creará objetos tipo Tiburón y la factoría de Leones, crearán objetos tipo León.

Por último, especificamos una Fábrica global que utilizará las 3 fábricas para crear los objetos (Aquí se aplica también el patrón Facade):

AnimalFactory.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public abstract class AnimalFactory {
  4.  
  5.         public static Animal create(AbstractAnimalFactory factory) {
  6.                 return factory.createAnimal();
  7.         }
  8. }

Ésta clase recibe un objeto que implemente la interfaz AbstractAnimalFactory, por lo tanto podrá recibir objetos tipo: DogFactory, SharkFactory y LionFactory. Luego simplemente usa la factoría especificada para crear un animal mediante polimorfismo.

Ahora construyamos nuestra clase principal para probar el funcionamiento del patrón:

FactoryTest.java
Código: Java
  1. package org.underc0de.factory;
  2.  
  3. public class FactoryTest {
  4.  
  5.         public static void main(String[] args) {
  6.                 Animal dog = AnimalFactory.create(new DogFactory("Perro","Caninos","Doméstico"));
  7.                 Animal shark = AnimalFactory.create(new SharkFactory("Tiburón", "Lámnidos", "Mar"));
  8.                 Animal lion = AnimalFactory.create(new LionFactory("León", "Felinos", "Selva"));
  9.                
  10.                 System.out.println(dog.toString());
  11.                 System.out.println();
  12.                 System.out.println(shark.toString());
  13.                 System.out.println();
  14.                 System.out.println(lion.toString());
  15.         }
  16. }

Utilizamos el método estático create() de AnimalFactory que recibe una implementación de la interfaz AbstractAnimalFactory, en éste caso un objeto DogFactory al que se le asignan los valores “Perro”, “Caninos” y “Doméstico” y finalmente AnimalFactory llama al método createAnimal() de DogFactory para crear un animal tipo Dog y devolverlo hacia AnimalFactory que lo devuelve y lo guarda en el objeto 'dog'. El mismo procedimiento es para todas las factorías.

Salida:

Código: [Seleccionar]
Tipo de animal: Perro
Familia: Caninos
Hábitat: Doméstico

Tipo de animal: Tiburón
Familia: Lámnidos
Hábitat: Mar

Tipo de animal: León
Familia: Felinos
Hábitat: Selva

Y de ésta manera sencilla, podemos usar tantas fábricas como queramos para poder crear objetos si generar dependencias


PATRÓN SINGLETON


Cuándo usarlo:

  • Cuando la aplicación requiere que solo exista una instancia de un determinado objeto.
Problema: Encapsular la configuración de una aplicación en un objeto y compartirlo con los demás objetos de la aplicación que lo requiera.

Solución: Utilizar el patrón Singleton para encapsular la configuración de la aplicación.

Para utilizar éste patrón se deben seguir dos reglas:

  • El primer paso es hacer el constructor privado para que no se pueda llamar y por lo tanto, no se puedan crear instancias.
  • El segundo paso es crear una instancia de la clase y devolverla mediante un método estático.


Teniendo en cuenta éstas reglas, creamos una clase que representa a la configuración de la aplicación:

Configuration.java
Código: Java
  1. package org.underc0de.singleton;
  2.  
  3. import java.util.HashMap;
  4. import java.util.Map;
  5.  
  6. public class Configuration {
  7.  
  8.         private Map<String,Object> appOptions = null;
  9.         private static Configuration config;
  10.        
  11.         private Configuration() {
  12.                
  13.         }
  14.        
  15.         public static Configuration getConfiguration() {
  16.                 if(config == null) {
  17.                         config = new Configuration();
  18.                 }
  19.                 return config;
  20.         }
  21.  
  22.         public Map<String,Object> getAppOptions() {
  23.                 if(appOptions == null) {
  24.                         appOptions = new HashMap<>();
  25.                         appOptions.put("theme", "dark");
  26.                         appOptions.put("show_hidde_files", true);
  27.                 }
  28.                 return appOptions;
  29.         }
  30.  
  31.         public void setAppOptions(Map<String,Object> appOptions) {
  32.                 this.appOptions = appOptions;
  33.         }
  34.        
  35. }

Y simplemente, cuando la necesitemos, obtenemos su única instancia:

SingletonTest.java
Código: Java
  1. package org.underc0de.singleton;
  2.  
  3. import java.util.Map;
  4.  
  5. public class SingletonTest {
  6.  
  7.         public static void main(String[] args) {
  8.                 Configuration cfg = Configuration.getConfiguration();
  9.                
  10.                 // recorre el hashmap para leer las claves y valores
  11.                 for(Map.Entry<String, Object> entry: cfg.getAppOptions().entrySet()) {
  12.                         System.out.println(entry.getKey()+": "+entry.getValue());
  13.                 }
  14.  
  15.         }
  16.  
  17. }

Salida:

Código: [Seleccionar]
show_hidde_files: true
theme: dark


NOTA IMPORTANTE:

Generalmente, cuando se usa éste patrón, se garantiza que solamente existirá una instancia de dicha clase. Pero, si se trata de una aplicación web y se va a integrar una aplicación web con otra, es probable que ya no exista una sola instancia.

Ésto se debe a los ClassLoaders. Cada contenedor de cada aplicación web (WAR) tiene su propio ClassLoader, por lo que en el supuesto caso de una integración de WARs, las instancias serán dos y no una como se había previsto.

Una mejor forma de de implementar el patrón Singleton es mediante un enum, como recomienda Joshua Bloch. Al ser un enum, todas sus propiedades serán constantes únicas, no se pueden crear nuevas, solo utilizar las ya descritas.

Código: Java
  1. public enum Configuration {
  2.         INSTANCE;
  3.        
  4.         private Map<String, Object> options;
  5.        
  6.         public Map<String, Object> getOptions() {
  7.                 if(options == null)
  8.                         fillOptions();
  9.                 return options;
  10.         }
  11.        
  12.         private void fillOptions() {
  13.                 options = new HashMap<>();
  14.                 options.put("theme", "Dark");
  15.                 options.put("show_hidden_files", true);
  16.         }
  17.        
  18. }

Post Original: https://underc0de.org/foro/java/patrones-de-diseno/