Ya hemos revisado el uso de un objeto singleton, pero es posible que todavía no se haya dado cuenta de que esta estrategia es un patrón de diseño, y uno de los más utilizados además.

Hay muchos de estos patrones, y se pueden clasificar según su propósito específico.

Clasificación de patrones

Tipo de patrón Aplicación
Creador Un tipo que resuelve el problema de creación de objetos
Estructural Patrones que nos permiten construir una jerarquía de clases correcta y extensible en nuestra arquitectura
Conductual Este conjunto de patrones facilita una interacción segura y conveniente entre los objetos en un programa.

Normalmente, un patrón se caracteriza por el problema que resuelve. Veamos unos cuantos patrones que encontramos con más frecuencia cuando trabajamos con Java:

Patrón Propósito
Singleton Ya estamos familiarizados con este patrón: lo usamos para crear y acceder a una clase que no puede tener más de una instancia.
Iterator También estamos familiarizados con este patrón. Sabemos que nos permite iterar sobre un objeto de colección sin revelar su representación interna. Se usa con colecciones.
Adapter Este patrón conecta objetos incompatibles para que puedan funcionar juntos. Creo que el nombre del patrón de adaptador te ayuda a imaginar exactamente lo que hace. Aquí hay un ejemplo sencillo de la vida real: un adaptador USB para un enchufe
Método de plantilla

Un patrón de programación de comportamiento que resuelve el problema de integración y permite cambiar los pasos algorítmicos sin cambiar la estructura del algoritmo.

Imagina que tenemos un algoritmo de ensamblaje de automóviles en forma de una secuencia de pasos de ensamblaje:

Chasis -> Carrocería -> Motor -> Interior de la cabina

Si colocamos un marco reforzado, un motor más potente o un interior con iluminación adicional, no tenemos que cambiar el algoritmo, y la secuencia abstracta sigue siendo la misma.

Decorador Este patrón crea envoltorios para objetos para darles funcionalidad útil. Lo consideraremos como parte de este artículo.

En Java.io, las siguientes clases implementan patrones:

Patrón Donde se utiliza en java.io
Adaptador
Método de plantilla
Decorador

Patrón Decorador

Imaginemos que estamos describiendo un modelo para un diseño de hogar.

En general, el enfoque se ve así:

Inicialmente, tenemos la opción de varios tipos de casas. La configuración mínima es una planta con un techo. Luego usamos todo tipo de decoradores para cambiar parámetros adicionales, lo que naturalmente afecta el precio de la casa.

Crearemos una clase abstracta Casa:


public abstract class House {
	String info;
 
	public String getInfo() {
    	return info;
	}
 
	public abstract int getPrice();
}
    

Aquí tenemos 2 métodos:

  • getInfo() devuelve información sobre el nombre y las características de nuestra casa;
  • getPrecio() devuelve el precio de la configuración actual de la casa.

También tenemos implementaciones estándar de Casa, como ladrillo y madera:


public class BrickHouse extends House {
 
	public BrickHouse() {
    	info = "Brick House";
	}
 
	@Override
	public int getPrice() {
    	return 20_000;
	}
}
 
public class WoodenHouse extends House {
 
	public WoodenHouse() {
    	info = "Wooden House";
	}
 
	@Override
	public int getPrice() {
    	return 25_000;
	}
}
    

Ambas clases heredan de la clase House y reemplazan su método de precio, estableciendo un precio personalizado para una casa estándar. Establecemos el nombre en el constructor.

A continuación, necesitamos escribir clases de decoradores. Estas clases también heredarán de la clase House. Para hacerlo, creamos una clase de decorador abstracta.


abstract class HouseDecorator extends House {
}
    

A continuación, creamos implementaciones de decoradores. Crearemos varias clases que nos permitirán agregar características adicionales a la casa:


public class SecondFloor extends HouseDecorator {
	House house;
 
	public SecondFloor(House house) {
    	this.house = house;
	}
 
	@Override
	public int getPrice() {
    	return house.getPrice() + 20_000;
	}
 
	@Override
	public String getInfo() {
    	return house.getInfo() + " + second floor";
	}
}
    
Un decorador que agrega un segundo piso a la casa

El constructor del decorador acepta una casa que "decoraremos", es decir, a la que agregaremos modificaciones. Y anulamos los métodos getPrice() y getInfo(), devolviendo información sobre la nueva casa actualizada basada en la anterior.


public class Garage extends HouseDecorator {
 
	House house;
	public Garage(House house) {
    	this.house = house;
	}
 
	@Override
	public int getPrice() {
    	return house.getPrice() + 5_000;
	}
 
	@Override
	public String getInfo() {
    	return house.getInfo() + " + garage";
	}
}
    
Un decorador que agrega un garage a nuestra casa

Ahora podemos actualizar nuestra casa con decoradores. Para hacerlo, necesitamos crear una casa:


House brickHouse = new BrickHouse();
    

A continuación, establecemos nuestra variable casa igual a un nuevo decorador, pasando nuestra casa:


brickHouse = new SecondFloor(brickHouse); 
    

Nuestra variable casa ahora es una casa con una segunda planta.

Veamos algunos casos de uso que involucran decoradores:

Código de ejemplo Salida

House brickHouse = new BrickHouse(); 

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());
                    

Brick House

20000


House brickHouse = new BrickHouse(); 

  brickHouse = new SecondFloor(brickHouse); 

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());
                    

Brick House + second floor

40000


House brickHouse = new BrickHouse();
 

  brickHouse = new SecondFloor(brickHouse);
  brickHouse = new Garage(brickHouse);

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());
                    

Brick House + second floor + garage

45000


House woodenHouse = new SecondFloor(new Garage(new WoodenHouse())); 

  System.out.println(woodenHouse.getInfo());
  System.out.println(woodenHouse.getPrice());
                    

Wooden House + garage + second floor

50000


House woodenHouse = new WoodenHouse(); 

  House woodenHouseWithGarage = new Garage(woodenHouse);

  System.out.println(woodenHouse.getInfo());
  System.out.println(woodenHouse.getPrice());

  System.out.println(woodenHouseWithGarage.getInfo());
  System.out.println(woodenHouseWithGarage.getPrice());
                    

Wooden House

25000

Wooden House + garage

30000

Este ejemplo ilustra el beneficio de actualizar un objeto con un decorador. Así, no cambiamos el objeto woodenHouse en sí, sino que creamos un nuevo objeto basado en el anterior. Aquí podemos ver que las ventajas conllevan desventajas: cada vez que creamos un nuevo objeto, aumentamos el consumo de memoria.

Echemos un vistazo a este diagrama UML de nuestro programa:

Un decorador tiene una implementación súper sencilla y cambia dinámicamente los objetos, mejorándolos. Los decoradores se pueden reconocer por sus constructores, que aceptan como parámetros objetos del mismo tipo abstracto o interfaz que la clase actual. En Java, este patrón se utiliza ampliamente en las clases de E/S.

Por ejemplo, como ya hemos señalado, todas las subclases de java.io.InputStream, OutputStream, Reader y Writer tienen un constructor que acepta objetos de las mismas clases.