Ir al contenido
wannacode

La tienda: qué patrón dónde

En resumen: Un ejercicio de patrones de diseño: una tienda con pago, avisos y almacenamiento en un solo método se reordena con Strategy, Observer y Repository, y su salida sigue igual.

Problema

Una tienda en un solo método

Aquí tienes una tienda en línea pequeña. El método del pedido calcula el importe según la forma de pago, guarda el pedido, envía un correo y escribe en el almacén:

class Shop {
private final List<String> orders = new ArrayList<>();
void placeOrder(String id, String customer, double amount, String method) {
double total;
if (method.equals("tarjeta")) {
total = amount;
} else if (method.equals("contra reembolso")) {
total = amount + 2.50;
} else {
throw new IllegalArgumentException("Forma de pago desconocida: " + method);
}
orders.add(id + ";" + customer + ";" + total);
System.out.printf("Correo a %s: pedido %s, %.2f €%n", customer, id, total);
System.out.println("Almacén: preparar " + id);
}
int count() {
return orders.size();
}
}
Shop shop = new Shop();
shop.placeOrder("A-1", "ana@example.com", 80, "tarjeta");
shop.placeOrder("A-2", "ivan@example.com", 45, "contra reembolso");
System.out.println("Pedidos: " + shop.count());
Salida
Correo a ana@example.com: pedido A-1, 80.00 €
Almacén: preparar A-1
Correo a ivan@example.com: pedido A-2, 47.50 €
Almacén: preparar A-2
Pedidos: 2

Funciona. Pero el mes que viene llegan tres cambios: transferencia bancaria con un 2 % de descuento, un SMS tras cada pedido y una base de datos en lugar de una lista. Los tres están en el mismo método. Cada uno lo toca, y no se puede escribir una prueba del importe sin que salga un correo.

En esta lección no hay un patrón nuevo. Es un ejercicio: reconoces cuál de los patrones del módulo ordena cada una de estas cosas, y cuáles no hacen falta aquí.

La idea

El síntoma da nombre al patrón

No eliges un patrón de una lista. Lo eliges por lo que duele:

Síntoma en el código Patrón
una cadena de if sobre un tipo que crece con cada variante nueva Strategy, o Factory si la cadena elige qué crear
tras un evento, el propio código llama a todos los que deben enterarse Observer
el almacenamiento está dentro de las reglas y una prueba sin base de datos es imposible Repository y pasarlo desde fuera
un switch sobre el texto del estado en cada método State
un constructor con muchos parámetros opcionales Builder
una biblioteca ajena con una interfaz incómoda Adapter
cinco subsistemas que hay que llamar en un orden exacto Facade
comportamiento «alrededor» de un método: registro, caché, añadidos al precio Decorator
deshacer la última acción Command
un árbol donde una parte y el todo se usan igual Composite

Si no encuentras el síntoma, lo más probable es que no necesites un patrón.

Pruébalo

Reconócelo

Mira otra vez la tienda de la primera tarjeta.

1. Una cadena de if sobre el texto de la forma de pago que crece con cada forma nueva. ¿Qué patrón la ordena?
2. Tras un pedido, el propio método envía un correo y escribe en el almacén. Mañana llegará también el SMS. ¿Qué patrón?
3. Los pedidos se guardan en una lista dentro de la tienda. Mañana necesitas una base de datos, y para la prueba quieres algo en memoria. ¿Qué patrón?
4. El pedido tiene tres campos. ¿Necesita un Builder?

Míralo

Una forma posible de ordenarla

Tres de los síntomas están en la tienda, así que también son tres los patrones.

Las formas de pago son estrategias. La interfaz tiene un método, así que cada forma es una lambda en la tabla Map:
interface PaymentMethod {
double total(double amount);
}
El pedido es un record. El almacenamiento está detrás de una interfaz, con una implementación para la memoria:
record Order(String id, String customer, double total) {}
interface OrderRepository {
void save(Order order);
int count();
}
class InMemoryOrders implements OrderRepository {
private final List<Order> orders = new ArrayList<>();
public void save(Order order) {
orders.add(order);
}
public int count() {
return orders.size();
}
}
interface OrderListener {
void placed(Order order);
}
La tienda recibe el almacenamiento y la tabla de formas desde fuera. Lleva una lista de suscriptores y avisa a cada uno cuando se hace un pedido:
class Shop {
private final OrderRepository orders;
private final Map<String, PaymentMethod> methods;
private final List<OrderListener> listeners = new ArrayList<>();
Shop(OrderRepository orders, Map<String, PaymentMethod> methods) {
this.orders = orders;
this.methods = methods;
}
void subscribe(OrderListener listener) {
listeners.add(listener);
}
void placeOrder(String id, String customer, double amount, String method) {
PaymentMethod payment = methods.get(method);
if (payment == null) {
throw new IllegalArgumentException("Forma de pago desconocida: " + method);
}
Order order = new Order(id, customer, payment.total(amount));
orders.save(order);
for (OrderListener listener : listeners) {
listener.placed(order);
}
}
int count() {
return orders.count();
}
}

Qué almacenamiento, qué formas y qué suscriptores se decide en un solo lugar, donde se monta la tienda:

Map<String, PaymentMethod> methods = Map.of(
"tarjeta", amount -> amount,
"contra reembolso", amount -> amount + 2.50);
Shop shop = new Shop(new InMemoryOrders(), methods);
shop.subscribe(o -> System.out.printf("Correo a %s: pedido %s, %.2f €%n", o.customer(), o.id(), o.total()));
shop.subscribe(o -> System.out.println("Almacén: preparar " + o.id()));
shop.placeOrder("A-1", "ana@example.com", 80, "tarjeta");
shop.placeOrder("A-2", "ivan@example.com", 45, "contra reembolso");
System.out.println("Pedidos: " + shop.count());
Salida
Correo a ana@example.com: pedido A-1, 80.00 €
Almacén: preparar A-1
Correo a ivan@example.com: pedido A-2, 47.50 €
Almacén: preparar A-2
Pedidos: 2

La salida es la misma, línea por línea. Ordenar no ha cambiado el comportamiento, y eso es lo primero que compruebas. Aquí los dos programas se ejecutan en cada comprobación del sitio y dan la misma salida.

Míralo

Los tres cambios ahora

La transferencia bancaria es una fila nueva en la tabla. El SMS es un suscriptor nuevo. La base de datos será una implementación nueva de OrderRepository. Ni una sola línea de la clase de la tienda cambia:

Map<String, PaymentMethod> methods = Map.of(
"tarjeta", amount -> amount,
"contra reembolso", amount -> amount + 2.50,
"transferencia bancaria", amount -> amount * 0.98);
Shop shop = new Shop(new InMemoryOrders(), methods);
shop.subscribe(o -> System.out.println("SMS a " + o.customer() + ": " + o.id()));
shop.placeOrder("A-3", "ana@example.com", 100, "transferencia bancaria");
Salida
SMS a ana@example.com: A-3

Ahora también es fácil una prueba del importe: pasas un almacenamiento en memoria y no suscribes el correo.

La idea

Dónde parar

El módulo tiene más de diez patrones, y aquí se usan tres. Los demás no se han olvidado, simplemente aquí no ayudan:

  • Builder: el pedido tiene tres campos obligatorios. Basta el constructor (record);
  • Singleton: nada tiene que ser el único. El almacenamiento se pasa desde fuera, así que la prueba y la tienda pueden tener cada una el suyo;
  • Factory: las formas de pago son funciones en una tabla, y la tabla ya es el lugar donde se hace la elección;
  • State: el pedido aún no tiene estados. Cuando lleguen «pagado» y «enviado», vendrá también (mira las tareas).

Paras cuando el cambio que viene se vuelve fácil. Más patrones no significa mejor código.

Ejercicio

Sigue ordenando la tienda

Resuelve las tareas en tu editor, partiendo de la tienda ordenada. Después de cada paso ejecuta el programa de la tarjeta «Una forma posible de ordenarla»: su salida debe seguir igual.

1. Los estados del pedido. El pedido pasa de nuevo → pagado → enviado y se puede cancelar, pero no después de enviarlo. Hazlo con State (o con un enum y una tabla de transiciones, si los estados no tienen comportamiento propio) y muestra el error al cancelar un pedido enviado.

2. Un descuento sobre 100 €. Un pedido de más de 100 € recibe un 5 % de descuento, sea cual sea el pago. Piensa dónde le corresponde estar: en una estrategia, en un Decorator alrededor de la estrategia o en la tienda. ¿Por qué?

3. Un almacenamiento que recuerda. Haz un segundo almacenamiento que escriba los pedidos en un archivo (uno por línea) y pásaselo a la tienda en lugar del de memoria. La clase de la tienda no cambia.

4. Un mensajero ajeno. El almacén ahora se llama a través de la biblioteca de un mensajero con un método createShipment(reference, weightGrams). El suscriptor del almacén pasa a ser un Adapter hacia ella.

Test

Ponte a prueba

1. Ordenas el código con patrones. ¿Cómo sabes que no has roto nada?
2. ¿Cuál NO es un buen motivo para añadir un patrón?
3. Tras ordenar, añades transferencia bancaria y SMS. ¿Qué tocas en la clase de la tienda?

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