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:
class Checkout { rules = new Map([ ["tarjeta", (amount) => amount], ["contra reembolso", (amount) => amount + 2.5], ["cupón", (amount) => Math.max(amount - 10, 0)], ]);
pay(method, amount) { return this.rules.get(method)(amount); }}const checkout = new Checkout();console.log(`${checkout.pay("contra reembolso", 80).toFixed(2)} €`);console.log(`${checkout.pay("cupón", 80).toFixed(2)} €`);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 funciones o 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:
- De creación: cómo y dónde se hacen los objetos. Factory Method y Simple Factory, Builder, Singleton y Dependency Injection.
- Estructurales: cómo se juntan los objetos en algo más grande. Adapter y Facade, Decorator, Composite.
- De comportamiento: quién llama a quién y quién decide qué. Strategy, Observer, Command, State, Template Method e Iterator.
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ú.
En JavaScript algunos patrones casi desaparecen, porque el lenguaje ya los tiene. Una función es un valor, así que Strategy y Command suelen ser solo una función. Recorrer una colección (Iterator) viene dentro defor. Cada lección del módulo dice dónde pasa eso, en vez de escribir Java con otra sintaxis.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:
class DiscountStrategy { apply(amount) { throw new Error("apply is not implemented"); }}
class PercentOverThresholdDiscountStrategy extends DiscountStrategy { constructor(percent, threshold) { super(); this.percent = percent; this.threshold = threshold; }
apply(amount) { return amount > this.threshold ? amount * (1 - this.percent / 100) : amount; }}
class AbstractDiscountStrategyFactory { createStrategy() { throw new Error("createStrategy is not implemented"); }}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory { createStrategy() { return new PercentOverThresholdDiscountStrategy(10, 100); }}
class DiscountService { constructor(factory) { this.factory = factory; }
discounted(amount) { return this.factory.createStrategy().apply(amount); }}const service = new DiscountService(new DefaultDiscountStrategyFactory());console.log(`Total: ${service.discounted(120).toFixed(2)} €`);console.log(`Total: ${service.discounted(80).toFixed(2)} €`);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:
function discounted(amount) { return amount > 100 ? amount * 0.9 : amount;}console.log(`Total: ${discounted(120).toFixed(2)} €`);console.log(`Total: ${discounted(80).toFixed(2)} €`);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.
En JavaScript esto es aún más claro: una clase que lanza «not implemented» para parecer abstracta es Java escrito en JavaScript. Una función ya es un valor que puedes pasar y guardar.Pruébalo
Qué familia, y si hace falta 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 clase con un solo método donde bastaría una función, una fábrica para una sola clase, una tabla con una sola regla. Todos los que leen el código pagan por ello, y nadie lo usa.
- El nombre dice el patrón, no el trabajo.
AbstractDiscountStrategyFactorydice cómo está hecho,discounteddice qué hace. Si no puedes describir la clase sin las palabras del patrón, probablemente no la necesitas. - Un patrón de otro lenguaje. Un patrón de Java copiado clase por clase: una clase abstracta, una fábrica y una clase con un solo método.
- El patrón antes del dolor. El buen orden es el contrario. Escribes lo simple. Cuando el mismo
switchaparece 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 JavaScript moderno?) y, en una frase, qué problema resuelve. Uno de los tres no necesita ningún patrón.
A.
class Order { listeners = [];
onShipped(listener) { this.listeners.push(listener); }
ship(id) { for (const listener of this.listeners) { listener(id); } }}const order = new Order();order.onShipped((id) => console.log(`Correo: el pedido ${id} fue enviado`));order.onShipped((id) => console.log(`Almacén: quita ${id} de la cola`));order.ship("1042");Correo: el pedido 1042 fue enviadoAlmacén: quita 1042 de la colaB.
class CourierApi { sendParcel(city, grams) { return `Paquete para ${city}, ${grams} g`; }}
class CourierShipping { api = new CourierApi();
ship(city, kilograms) { return this.api.sendParcel(city, Math.round(kilograms * 1000)); }}const shipping = new CourierShipping();console.log(shipping.ship("Sevilla", 1.25));Paquete para Sevilla, 1250 gC.
function priceLabel(price, withVat) { if (withVat) { return `${(price * 1.2).toFixed(2)} € con IVA`; } return `${price.toFixed(2)} € sin IVA`;}console.log(priceLabel(25, true));console.log(priceLabel(25, false));30.00 € con IVA25.00 € sin IVALas respuestas
- A es Observer. El pedido no sabe quién lo escucha. El correo y el almacén se suscriben, y
shiplos llama en el orden en que se suscribieron. Un oyente nuevo no tocaOrder. - B es Adapter.
CourierShippingtraduce tuship(kilogramos) alsendParcelajeno (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 dos funciones y una tabla 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
switcho una cadena deifque se repita en dos lugares. ¿Con qué palabra lo nombrarías en una revisión?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Arrays: push, splice y sort · Map y Set · Funciones flecha · Patrones de diseño
- Ejercicios sobre el tema: Patrones de diseño