Ir al contenido
wannacode

Por qué patrones

En resumen: Un patrón de diseño es una solución con nombre y probada para un problema que se repite en el código; los 23 patrones clásicos forman tres familias: de creación, estructurales y de comportamiento.

Problema

Dos párrafos para una sola palabra

Una revisión de código en una tienda en línea. Pablo lee la caja y deja un comentario en la línea marcada:

interface PaymentRule {
double total(double amount);
}
class Checkout {
private final Map<String, PaymentRule> rules = Map.of(
"tarjeta", amount -> amount,
"contra reembolso", amount -> amount + 2.50,
"cupón", amount -> Math.max(amount - 10, 0));
public double pay(String method, double amount) {
return rules.get(method).total(amount);
}
}
Checkout checkout = new Checkout();
System.out.printf("%.2f €%n", checkout.pay("contra reembolso", 80));
System.out.printf("%.2f €%n", checkout.pay("cupón", 80));
Salida
82.50 €
70.00 €

Pablo: No entiendo qué es esto. La caja guarda una tabla en la que cada método de pago es una regla aparte. Todas las reglas tienen la misma forma: toman una suma y devuelven una suma. Mientras el programa corre, la caja toma la regla por el nombre que eligió el cliente y la llama, sin saber cuál es. ¿Es a propósito? Si mañana llega el pago a plazos, ¿tengo que tocar pay?

María: Es a propósito. No tocas pay, añades una fila a la tabla. La regla solo calcula y no recuerda nada, por eso la caja puede preguntarle varias veces: para la vista previa, para la factura, para el pago. Si no, cada pregunta daría otra suma…

Dos personas, dos párrafos, y lo más importante sigue sin decirse: ¿qué pasa si el nombre no está en la tabla?

María podría haber escrito una palabra: Strategy. Pablo habría sabido qué estaba viendo y qué revisar. La regla solo calcula. Un método nuevo es una fila nueva, y la caja no se toca. Un nombre desconocido se atrapa antes de llegar a la tabla. Mucha gente ya resolvió estas preguntas, y están escritas bajo ese nombre.

La idea

Un patrón es el nombre de una solución

Un patrón de diseño es una solución probada para un problema que aparece en muchos proyectos, con un nombre que todos conocen. No es una biblioteca ni código para copiar: cada proyecto lo escribe a su manera. Un patrón lleva cuatro cosas:

  • un nombre: una palabra en lugar de dos párrafos;
  • el problema que resuelve: por ejemplo «la regla cambia, y la caja no debe tocarse»;
  • los roles: quién guarda qué y quién llama a quién;
  • el costo: lo que pagas por él (más clases, un salto más al leer) y cuándo no vale la pena.

La mayoría de los nombres vienen del libro «Design Patterns» (1994), de cuatro autores a quienes los desarrolladores llaman la «Gang of Four». Describe 23 patrones en tres familias. El módulo recorre los más usados:

Repository y las capas no es del libro. Es un patrón de arquitectura: dónde vive la base de datos en el programa.

El módulo termina con un ejercicio, La tienda: qué patrón dónde: una tienda desordenada que ordenas tú.

Míralo

La trampa: un patrón por el patrón

La tarea es una sola: un 10 % de descuento en pedidos de más de 100 €. Alguien acaba de leer sobre patrones:

interface DiscountStrategy {
double apply(double amount);
}
class PercentOverThresholdDiscountStrategy implements DiscountStrategy {
private final double percent;
private final double threshold;
public PercentOverThresholdDiscountStrategy(double percent, double threshold) {
this.percent = percent;
this.threshold = threshold;
}
@Override
public double apply(double amount) {
return amount > threshold ? amount * (1 - percent / 100) : amount;
}
}
abstract class AbstractDiscountStrategyFactory {
public abstract DiscountStrategy createStrategy();
}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory {
@Override
public DiscountStrategy createStrategy() {
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService {
private final AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory) {
this.factory = factory;
}
public double discounted(double amount) {
return factory.createStrategy().apply(amount);
}
}
DiscountService service = new DiscountService(new DefaultDiscountStrategyFactory());
System.out.printf("Total: %.2f €%n", service.discounted(120));
System.out.printf("Total: %.2f €%n", service.discounted(80));
Salida
Total: 108.00 €
Total: 80.00 €

Cinco clases, dos patrones (Strategy y una fábrica) y un nombre como AbstractDiscountStrategyFactory. La misma tarea, sin patrón:

static double discounted(double amount) {
return amount > 100 ? amount * 0.9 : amount;
}
System.out.printf("Total: %.2f €%n", discounted(120));
System.out.printf("Total: %.2f €%n", discounted(80));
Salida
Total: 108.00 €
Total: 80.00 €

La salida es la misma. La diferencia está en la lectura: para saber de cuánto es el descuento, en la primera versión pasas por cinco clases, y en la segunda lees una línea.

Los patrones de la primera versión no resuelven ningún problema. Hay un solo descuento, nadie lo cambia mientras el programa corre y nadie elige entre descuentos. Cuando llega el segundo descuento y el cliente tiene que elegir, ahí es cuando la función se convierte en una estrategia. No antes.

Pruébalo

Qué familia, y si hace falta un patrón

1. Builder, Singleton y Factory Method pertenecen a una misma familia. ¿A cuál?
2. La biblioteca del servicio de mensajería quiere sendParcel(city, grams), y tu código llama a ship(city, kilograms). ¿De qué familia es la solución?
3. La etiqueta muestra el precio «con IVA» o «sin IVA». Se decide en un solo lugar, y las dos variantes no han cambiado en años. ¿Necesitas un patrón?
4. El cliente elige uno de cinco métodos de pago, y cada mes llega uno nuevo. ¿Necesitas un patrón?

La idea

Cuándo un patrón estorba

Un patrón es la respuesta a un problema. Sin el problema, solo queda el costo:

  • «Quizá lo necesitemos». Una interfaz con una sola implementación, una fábrica para una sola clase, una estrategia con una sola variante. Todos los que leen el código pagan por ello, y nadie lo usa.
  • El nombre dice el patrón, no el trabajo. AbstractDiscountStrategyFactory dice cómo está hecho, discounted dice qué hace. Si no puedes describir la clase sin las palabras del patrón, probablemente no la necesitas.
  • Un patrón de otro lenguaje. Donde bastaría una lambda, una interfaz y una clase por cada regla son demasiado.
  • El patrón antes del dolor. El buen orden es el contrario. Escribes lo simple. Cuando el mismo switch aparece en un tercer lugar o las reglas pasan a ser cinco, refactorizas hacia el patrón. Para entonces ya sabes exactamente qué cambia.

Los patrones se aprenden para reconocerlos y nombrarlos en el código ajeno y en las revisiones. No para ponerlos en todas partes.

Ejercicio

Reconoce el patrón

Resuélvelo en tu editor.

Tres fragmentos de una misma tienda. Ejecútalos y di de cada uno: ¿hay un patrón?, ¿cuál es (Strategy, Observer, Factory Method, Singleton o Adapter, de la lección «Patrones de diseño» del módulo Java moderno?) y, en una frase, qué problema resuelve. Uno de los tres no necesita ningún patrón.

A.

class Order {
private final List<Consumer<String>> listeners = new ArrayList<>();
public void onShipped(Consumer<String> listener) {
listeners.add(listener);
}
public void ship(String id) {
for (Consumer<String> listener : listeners) {
listener.accept(id);
}
}
}
Order order = new Order();
order.onShipped(id -> System.out.println("Correo: el pedido " + id + " fue enviado"));
order.onShipped(id -> System.out.println("Almacén: quita " + id + " de la cola"));
order.ship("1042");
Salida
Correo: el pedido 1042 fue enviado
Almacén: quita 1042 de la cola

B.

class CourierApi {
public String sendParcel(String city, int grams) {
return "Paquete para " + city + ", " + grams + " g";
}
}
interface Shipping {
String ship(String city, double kilograms);
}
class CourierShipping implements Shipping {
private final CourierApi api = new CourierApi();
@Override
public String ship(String city, double kilograms) {
return api.sendParcel(city, (int) Math.round(kilograms * 1000));
}
}
Shipping shipping = new CourierShipping();
System.out.println(shipping.ship("Sevilla", 1.25));
Salida
Paquete para Sevilla, 1250 g

C.

static String priceLabel(double price, boolean withVat) {
if (withVat) {
return String.format("%.2f € con IVA", price * 1.20);
}
return String.format("%.2f € sin IVA", price);
}
System.out.println(priceLabel(25, true));
System.out.println(priceLabel(25, false));
Salida
30.00 € con IVA
25.00 € sin IVA
Las respuestas
  • A es Observer. El pedido no sabe quién lo escucha. El correo y el almacén se suscriben, y ship los llama en el orden en que se suscribieron. Un oyente nuevo no toca Order.
  • B es Adapter. CourierShipping traduce tu ship (kilogramos) al sendParcel ajeno (gramos), sin tocar ninguno de los dos.
  • C no necesita patrón. Dos variantes, en un solo lugar, que nadie cambia. Strategy aquí sería una interfaz y dos clases en lugar de un solo if.

Más tareas

  • Cuándo C necesita un patrón. La tienda empieza a vender en cinco países con distinto IVA, y el cliente elige el país. ¿Qué cambia? Escribe qué patrón ayudaría y por qué, sin código.
  • El patrón que desaparece. Toma las cinco clases de la tarjeta «La trampa» y reemplázalas por una sola función. Cuenta las líneas antes y después. La salida debe seguir igual.
  • Tu código. Busca en uno de tus proyectos un switch o una cadena de if que se repita en dos lugares. ¿Con qué palabra lo nombrarías en una revisión?

Test

Ponte a prueba

1. ¿Qué es un patrón de diseño?
2. ¿Por qué ayuda el nombre de un patrón en una revisión de código?
3. ¿Qué señal indica que un patrón no hace falta?
4. ¿Cuál es el buen orden?
5. Adapter, Decorator y Composite son:

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·