Original article: The 3 Types of Design Patterns All Developers Should Know (with code examples of each)

¿Qué es un Patrón de Diseño?

Los patrones de diseño son soluciones a nivel de diseño para problemas recurrentes que nosotros como ingenieros de software enfrentamos frecuentemente. No es código - Repito ❌CÓDIGO. Es como una descripción sobre como atacar estos problemas y diseñar una solución.

Usar estos patrones es considerado una buena práctica, ya que el diseño de la solución ha sido probado y comprobado, resulta en una legibilidad más alta en el código final. Los patrones de diseño son frecuentemente creados y usados en lenguajes de Programación Orientada a Objetos, como Java, en el cuál la mayoría de los ejemplos estarán escritos desde ahora en adelante.

Tipos de patrones de diseño

Hay cerca de 26 patrones actualmente descubiertos (difícilmente creo que los cubriré todos...).

Estos 26 puede ser clasificados en 3 tipos:

  1. Creacionales: Estos patrones son diseñados para la instanciación de clases. Pueden ser patrones de creación tanto de clases como de objetos.
  2. Estructurales: Estos patrones son diseñados con respecto a la estructura y composición de una clase. La principal meta de la mayoría de estos patrones es la de incrementar la funcionalidad de la(s) clase(s) involucrada(s), sin cambiar mucho de su composición.
  3. De comportamiento: Estos patrones son diseñados dependiendo de cómo una clase se comunica con otras.

En este artículo, veremos cada tipo de patrón de diseño en esta clasificación, empezando desde el más básico.

Tipo 1: Creacional - El patrón de diseño Singleton

El patrón de diseño Singleton es un patrón creacional, del cual su objetivo es crear una sola instancia de una clase y proveer un solo punto de acceso global para ese objeto. Un ejemplo comúnmente usado es la clase Calendar en Java, donde no puedes hacer una instancia de esa clase. También utiliza su propio método getInstance() para obtener el objeto a ser usado.

Una clase que use el patrón de diseño singleton incluirá:

singleton-class-diagram
Diagrama de clase Singleton
  1. Una variable estática, que contiene la única instancia de la clase
  2. Un constructor privado, para que así no se pueda instanciar en ningún otro lado.
  3. Un método publico estático, para retornar la única instancia de la clase.

Hay muchas maneras diferentes de implementar el diseño singleton. Hoy cubriré cada una de las implementaciones de:

1. Instanciación ansiosa

2. Instanciación perezosa

3. Instanciación segura para subprocesos

Castor ansioso

public class EagerSingleton {
	// crear una instancia de la clase.
	private static EagerSingleton instance = new EagerSingleton();

	//constructor privado para evitar que sea instanciado fuera de la clase.
	private EagerSingleton() {  }

	// devuelve la única instancia de la clase.
	public static EagerSingleton getInstance() {
		return instance;
	}
}

Este tipo de instanciación sucede durante la carga de la clase, ya que la creación de la instancia de la variable se produce fuera de cualquier método. Esto supone un gran inconveniente si la aplicación cliente no utiliza esta clase en absoluto. El plan de contingencia, si esta clase no es usada, es la instanciación perezosa.

Días de pereza

No hay tanta diferencia con la implementación de arriba. Las principales diferencias son que la variable estática es inicialmente declarada como nula, y es solo instanciada dentro del método getInstance() si, y solo si, la variable de la instancia permanece nula en el momento que se revisa.

public class LazySingleton {
	// inicializa la instancia con valor nulo.
	private static LazySingleton instance = null;

	// constructor privado para evitar que sea instanciada fuera de esta clase.
	private LazySingleton() {  }
	
    // revisa si la instancia es nula, si lo es crea el objeto.	
	public static LazySingleton getInstance() {
		if (instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
}

Esto soluciona un problema, pero otro sigue existiendo. ¿Qué tal si dos clientes diferentes acceden a la clase Singleton al mismo tiempo, en el mismo milisegundo? Bueno, ellos revisarían si la instancia es nula al mismo tiempo, y la encontrarían como verdadera, y entonces crearían dos instancias de la clase para cada petición de los dos clientes. Para solucionar esto, la instanciación segura de hilo debe ser implementada.

La seguridad (para subprocesos) es clave

En java, la palabra clave "synchronized" es usada en métodos u objetos para implementar seguridad de subprocesos, con la finalidad de que solo un hilo a la vez acceda a un recurso en particular. La instanciación de la clase es puesta dentro de un bloque sincronizado, para que el método solo pueda ser accesible por solo un cliente en un momento dado.

public class ThreadSafeSingleton {
	// inicializa la instancia con valor nulo.
	private static ThreadSafeSingleton instance = null;

	// constructor privado para evitar que sea instanciada fuera de esta clase.
	private ThreadSafeSingleton() {  }
    
	// revisa si la instancia es nula, dentro de un bloque síncrono, si lo es crea el objeto.	
	public static ThreadSafeSingleton getInstance() {
		synchronized (ThreadSafeSingleton.class) {
			if (instance == null) {
				instance = new ThreadSafeSingleton();
			}
		}
		return instance;
	}
}

La sobrecarga de procesamiento que resulta del método sincronizado es elevada y reduce el rendimiento de toda la operación.

Por ejemplo, si la variable de la instancia ya ha sido inicializada, entonces cada vez que cualquier cliente acceda al método getInstance(), el método synchronized es ejecutado y el rendimiento disminuye. Esto solo sucede para revisar si el valor de la variable instance es nula. Si se encuentra que, si es nula, se sale del método.

Para reducir la sobrecarga, se usa el doble cierre. La revisión se realiza antes del método synchronized  y si el valor es solo nulo, hace que el método synchronized se ejecute.

// se usa el doble cierre para reducir la sobrecarga del método síncrono ("synchronized")
public static ThreadSafeSingleton getInstanceDoubleLocking() {
	if (instance == null) {
		synchronized (ThreadSafeSingleton.class) {
			if (instance == null) {
				instance = new ThreadSafeSingleton();
			}
		}
	}
	return instance;
}

Ahora a la siguiente clasificación.

Tipo 2: Estructurales - El patrón de diseño Decorador

Te voy a dar un pequeño escenario para dar un mejor contexto del por qué y dónde deberías usar el patrón Decorador.

Imagina que tienes una cafetería, y como cualquier novato, empiezas con solo dos tipos de café simples, la mezcla de la casa y el negro tostado. En tu sistema de cobro, había una clase para las diferentes mezclas de café, las cuales heredan de la clase abstracta de bebidas. De hecho, la gente empieza a venir y a tomar tu maravilloso (aunque amargo) café. Entonces hay novatos del café que, dios no lo quiera, quieren azúcar o leche. ¿¿¡¡Qué burla de café!! ??

Ahora necesitas tener esos dos complementos también, ambos para el menú y desafortunadamente en el sistema de cobro. Originalmente, tu personal de TI hará una subclase para ambos cafés, una incluye azúcar, la otra leche. Entonces, ya que los clientes siempre están en lo correcto, uno de ellos dice estas temidas palabras:

"¿Me puede dar un café con leche y azúcar, por favor?"

???

Ahí va tu sistema de cobro riéndose en tu cara otra vez. Bueno, regresemos al pizarrón...

El personal de TI entonces añade café con leche y azúcar como otra subclase para cada clase padre de café. El resto del mes todo marcha sobre ruedas, la gente hace fila para beber tu café, ¿¿En realidad estás ganando dinero??

¡Pero espera, hay más!

El mundo en tu contra otra vez. Un competidor abre del otro lado de la calle, no con solo 4 tipos de café, ¡sino más de 10 complementos también!

Compras todos esos y más, para vender mejor café, y solo entonces recuerdas que olvidaste actualizar ese maldito sistema de cobro. Es muy posible que no puedas hacer el numero infinito de subclases para cada una de todas las combinaciones de todos los aditivos, con las nuevas mezclas de café también. Sin mencionar, el tamaño del sistema final.

Es tiempo de invertir en un sistema de cobro apropiado. Encuentras un nuevo personal de TI, quienes sí saben lo que están haciendo y te dicen:

Esto sería mucho más fácil y pequeño si se usa el patrón decorador.

¿Qué demonios es eso?

El patrón de diseño decorador cae en la categoría estructural, que lidia con la estructura real de una clase, ya sea por herencia, composición o ambos. La meta de este diseño es modificar la funcionalidad de un objeto en tiempo de ejecución. Esto es uno de otros muchos patrones de diseño que utiliza clases abstractas e interfaces con composición para obtener el resultado deseado.

Démosles la oportunidad a las matemáticas (¿temor?) para poner esto en perspectiva.

Digamos que hay 4 mezclas de café y 10 complementos. Si nos atoramos con la generación de subclases para cada diferente combinación de todos los complementos para un tipo de café. Eso es:

(10–1) ² = 9² = 81 subclases

Restamos 1 al 10, ya que no puedes combinar un complemento con otro del mismo tipo, azúcar con azúcar suena estúpido. Y eso es para una sola mezcla de café. ¡Multiplica ese 81 por 4 y obtendrás la enorme cantidad de 324 subclases diferentes! Piensa en todo el código que se tendría que escribir...

Pero con el patrón decorador se requerirán solo 16 clases en este escenario. ¿Quieres apostar?

decorator-class-diagram
Diagrama de clase del patrón de diseño Decorador
decorator-coffee-class-diagram
Diagrama de clase correspondiente al escenario de la cafetería

Si trazamos nuestro escenario según el diagrama de clase anterior, obtenemos 4 clases para las 4 mezclas de café, 10 para cada aditivo y 1 para el componente abstracto y una más para el decorador abstracto. ¡Ves! ¡16! ¡Ahora dame esos $100! (Solo bromeo, pero no te los rechazaría si me los dieras... solo digo)

Como puedes ver arriba, al igual que las mezclas concretas de café son subclases de la clase abstracta de bebida, la clase abstracta AddOn también hereda sus métodos de ella. Los complementos, que son sus subclases, a la vez heredan cualquier nuevo método para añadir funcionalidad al objeto base cuando sea necesario.

Vamos al código para ver este patrón en uso.

Primero haremos la clase abstracta de bebida, esa que todas las diferentes mezclas de café heredarán:

public abstract class Beverage {
	private String description;
    
	public Beverage(String description) {
		super();
		this.description = description;
	}
    
	public String getDescription() {
		return description;
	}
    
	public abstract double cost();
}

Entonces añadimos ambas mezclas de café concretas.

public class HouseBlend extends Beverage {
	public HouseBlend() {
		super(“House blend”);
	}

	@Override
	public double cost() {
		return 250;
	}
}

public class DarkRoast extends Beverage {
	public DarkRoast() {
		super(“Dark roast”);
	}

	@Override
	public double cost() {
		return 300;
	}
}

La clase abstracta de AddOn también hereda de la clase abstracta Beverage (más sobre esto abajo).

public abstract class AddOn extends Beverage {
	protected Beverage beverage;

	public AddOn(String description, Beverage bev) {
		super(description);
		this.beverage = bev;
	}

	public abstract String getDescription();
}

Y ahora la implementación concreta de esta clase abstracta:

public class Sugar extends AddOn {
	public Sugar(Beverage bev) {
		super(“Sugar”, bev);
	}

	@Override
	public String getDescription() {
		return beverage.getDescription() + “ with Mocha”;
	}

	@Override
	public double cost() {
		return beverage.cost() + 50;
	}
}

public class Milk extends AddOn {
	public Milk(Beverage bev) {
		super(“Milk”, bev);
	}

	@Override
	public String getDescription() {
		return beverage.getDescription() + “ with Milk”;
	}

	@Override  public double cost() {
		return beverage.cost() + 100;
	}
}

Como puedes ver arriba, podemos pasar cualquier subclase de Beverage a cualquier subclase de AddOn, y obtener el costo añadido, así como la descripción actualizada. Y, ya que la clase AddOn es esencialmente de tipo Beverage, podemos pasar un AddOn dentro de otro AddOn. De esta forma, podemos añadir cualquier número de complementos a una mezcla de café en específico.

Ahora escribimos algo de código para probar esto.

public class CoffeeShop {
	public static void main(String[] args) {
		HouseBlend houseblend = new HouseBlend();
		System.out.println(houseblend.getDescription() + “: “ + houseblend.cost());

		Milk milkAddOn = new Milk(houseblend);
		System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost());

		Sugar sugarAddOn = new Sugar(milkAddOn);
		System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost());
	}
}

El resultado final es:

decorator-final
P.D. esto está en rupias de Sri Lanka

¡Funciona! Somos capaces de añadir más de un complemento a una mezcla de café y actualizar su costo final y descripción exitosamente, sin la necesidad de hacer una cantidad infinita de subclases para cada combinación de complementos para todas las mezclas de café.

Finalmente, a la última categoría.

Tipo 3: De comportamiento - El patrón de diseño de Comandos

Un patrón de diseño de comportamiento se enfoca en como las clases y objetos se comunican entre ellas. El principal enfoque del patrón de comando es inculcar un menor grado de acoplamiento (también conocido como acoplamiento débil) entre las partes involucradas (léase: clases).

Uhhhh… ¿Qué es eso?

El acoplamiento es la forma en la que dos (o más) clases interactúan entre ellas. El escenario ideal cuando estas clases interactúan es que no dependan fuertemente una de otra. Eso es acoplamiento débil. Así que, una mejor definición para el acoplamiento débil sería: clases que están interconectadas, haciendo el menor uso entre ellas.

La necesidad de este patrón surgió cuando las peticiones necesitaban ser enviadas sin saber conscientemente que es lo que se pide o quien es el receptor.

En este patrón, la invocación de la clase esta desacoplada de la clase que realmente ejecuta una acción. La clase invocadora solo tiene el método invocable execute, el cuál corre el comando necesario, cuando el cliente lo solicita.

Veamos un ejemplo básico de la vida real ordenando un platillo en un restaurant elegante. Como el flujo va, tú le das tu orden (comando) al mesero (invocador), quien entonces entrega al chef (receptor), y es así como obtienes tu comida. Puede sonar sencillo... pero es algo aburrido de codificar.

chain-of-command-be-like-pop-snoke-im-going-to-27790631

La idea es bastante sencilla, pero la programación da mil vueltas.

command-class-diagram
Diagrama de clase del patrón de diseño Comando

El flujo de operación del lado técnico es: haces un comando concreto, el cual implementa la interfaz Command, pidiendo al receptor que complete una acción, y envía el comando al invocador. El invocador es la persona que sabe cuándo dar el comando. El chef es el único que sabe qué hacer cuando se le da una orden o un comando especifico. Entonces, cuando el método "execute" del invocador es ejecutado, este a su vez, hace que se ejecute el método "execute" del objeto del comando en el receptor, completando así las acciones necesarias.

Lo que necesitamos para implementarlo es:

  1. Una interfaz Command
  2. Una clase Order que implemente la interfaz Command
  3. Una clase Waiter (invocador)
  4. Una clase Chef (receptor)

Entonces, el código es el siguiente:

Chef, el receptor

public class Chef {
	public void cookPasta() {
		System.out.println(“Chef is cooking Chicken Alfredo…”);
	}

	public void bakeCake() {
		System.out.println(“Chef is baking Chocolate Fudge Cake…”);
	}
}

Command, la interfaz

public interface Command {
	public abstract void execute();
}

Order, el comando concreto

public class Order implements Command {
	private Chef chef;
	private String food;

	public Order(Chef chef, String food) {
		this.chef = chef;
		this.food = food;
	}

	@Override
	public void execute() {
		if (this.food.equals(“Pasta”)) {
			this.chef.cookPasta();
		} else {
			this.chef.bakeCake();
		}
	}
}

Waiter, el invocador

public class Waiter {
	private Order order;

	public Waiter(Order ord) {
		this.order = ord;
	}

	public void execute() {
		this.order.execute();
	}
}

Tú, el cliente

public class Client {
	public static void main(String[] args) {
		Chef chef = new Chef();
        
		Order order = new Order(chef, “Pasta”);
		Waiter waiter = new Waiter(order);
		waiter.execute();

		order = new Order(chef, “Cake”);
		waiter = new Waiter(order);
		waiter.execute();
	}
}

Como puedes ver arriba, la clase Client hace una Orden y establece al Chef como receptor. La Orden es enviada al Mesero, quien sabrá cuando ejecutar la Orden (por ejemplo, cuando dar la orden a cocinar al chef). Cuando el invocador es ejecutado, el método "execute" de la clase Order es ejecutado en el receptor (por ejemplo, al chef se le da una orden para cocinar pasta u hornear un pastel).

Repaso rápido

En este artículo hablamos de:

  1. Qué es realmente un patrón de diseño
  2. Los diferentes tipos de patrones de diseño y por qué son diferentes
  3. Un patrón de diseño básico o común de cada tipo

Espero que te haya sido de ayuda.

Encuentra el repositorio del código de este artículo aquí.