Strategy
En resumen: Strategy pone cada regla (por ejemplo una forma de pago) en su propia función, para añadir reglas nuevas sin tocar el código que las usa.
Problema
El cobro que conoce todas las formas de pago
La tienda en línea acepta cinco formas de pago. Contra reembolso cuesta 2,50 € más, un vale quita 10 €, una transferencia bancaria tiene un 2 % de descuento, y pagar a plazos cuesta un 5 % más. El cobro calcula el importe así:
function total(method, amount) { if (method === "tarjeta") { return amount; } else if (method === "contra reembolso") { return amount + 2.5; } else if (method === "vale") { return Math.max(amount - 10, 0); } else if (method === "transferencia bancaria") { return amount * 0.98; } else if (method === "pago a plazos") { return amount * 1.05; } throw new Error("Forma de pago desconocida: " + method);}console.log(`Tarjeta: ${total("tarjeta", 80).toFixed(2)} €`);console.log(`Contra reembolso: ${total("contra reembolso", 80).toFixed(2)} €`);console.log(`Vale: ${total("vale", 80).toFixed(2)} €`);console.log(`Transferencia bancaria: ${total("transferencia bancaria", 80).toFixed(2)} €`);console.log(`Pago a plazos: ${total("pago a plazos", 80).toFixed(2)} €`);Tarjeta: 80.00 €Contra reembolso: 82.50 €Vale: 70.00 €Transferencia bancaria: 78.40 €Pago a plazos: 84.00 €Funciona. Pero cada mes llega una forma nueva, y cada una es una rama nueva en esta función. La misma cadena está también en la factura y en el correo de confirmación. Alguien añade Apple Pay en dos de los tres lugares, y el cliente recibe un correo que dice «Forma de pago desconocida».
Los vales ya no son solo de 10 €. Para pasar el valor, la función recibe un tercer parámetro, y cambia cada lugar que lo llama, incluso en los pagos con tarjeta, donde no hay vale. El código por el que pasa cada pago se toca con cada cambio.
La idea
Strategy: la regla aparte, el cobro pregunta
Lo que cambia es una sola cosa: cómo se calcula el importe. Lo que no cambia es el cobro: toma el importe, pregunta y redondea.
Así que la regla pasa a una función aparte. Todas las formas comparten una forma común: reciben un importe y devuelven cuánto hay que pagar. El cobro guarda la función en un campo y la llama sin saber de qué función se trata.Este es el patrón Strategy. Tiene tres papeles:
- el cobro guarda la estrategia y la llama. Los libros lo llaman el contexto;
- la forma común: una función que recibe un importe y devuelve un importe;
- las estrategias concretas: una función para cada forma.
Una forma nueva es una estrategia nueva. El cobro no se toca.
Míralo
El cobro y tres formas de pago
voucher(10) crea una estrategia que recuerda sus 10 €. Así el valor no es un parámetro de cada forma:function card(amount) { return amount;}
function cashOnDelivery(amount) { return amount + 2.5;}
function voucher(value) { return (amount) => Math.max(amount - value, 0);}method y la llama. Asignar checkout.method la cambia:class Checkout { constructor(method) { this.method = method; }
total(amount) { return Math.round(this.method(amount) * 100) / 100; }}const checkout = new Checkout(card);console.log(`Tarjeta: ${checkout.total(80).toFixed(2)} €`);
checkout.method = cashOnDelivery;console.log(`Contra reembolso: ${checkout.total(80).toFixed(2)} €`);
checkout.method = voucher(10);console.log(`Vale: ${checkout.total(80).toFixed(2)} €`);Tarjeta: 80.00 €Contra reembolso: 82.50 €Vale: 70.00 €const checkout = new Checkout(card);console.log(`Tarjeta: ${checkout.total(80).toFixed(2)} €`);
checkout.method = cashOnDelivery;console.log(`Contra reembolso: ${checkout.total(80).toFixed(2)} €`);
checkout.method = voucher(10);console.log(`Vale: ${checkout.total(80).toFixed(2)} €`);Paso a paso, en palabras
- El cobro recibe una tarjeta: su campo method apunta a la función card.
- total(80): el cobro no conoce la forma de pago. Solo le pregunta a la estrategia el importe y recibe 80.00.
- Una forma nueva. checkout.method = … hace que el campo apunte a la función cashOnDelivery. En Checkout no cambia ni una línea.
- El mismo total(80), otra respuesta: 82.50.
- voucher(10) devuelve una función que recuerda value = 10. Ya nada apunta a las funciones antiguas.
- El vale quita 10: 70.00. Tres formas de pago, y el cobro es todo el tiempo el mismo objeto con el mismo código.
El cobro es un objeto con un solo trozo de código para todo el programa. Solo cambia aquello a lo que apunta el campo.
Míralo
Una forma nueva sin tocar el cobro
La transferencia bancaria y el pago a plazos llegan como dos funciones nuevas:
function bankTransfer(amount) { return amount * 0.98;}
function installments(amount) { return amount * 1.05;}const checkout = new Checkout(bankTransfer);console.log(`Transferencia bancaria: ${checkout.total(80).toFixed(2)} €`);
checkout.method = installments;console.log(`Pago a plazos: ${checkout.total(80).toFixed(2)} €`);Transferencia bancaria: 78.40 €Pago a plazos: 84.00 €Ni una sola línea del cobro ha cambiado. No puedes romper los pagos con tarjeta mientras añades transferencias, porque el código de la tarjeta no está delante de ti.
Aquí no hay un compilador que compruebe la forma. Si la función nueva recibe dos valores en lugar de uno, el error solo aparece cuando el cobro la llama. Por eso cada estrategia nueva se ejecuta al menos una vez antes de llegar a un cliente.
Míralo
Qué forma elige el cliente
El cliente escribe la forma como texto. La cadena de if que elegía la regla se convierte en una tabla: nombre → estrategia.
const methods = new Map([ ["tarjeta", card], ["contra reembolso", cashOnDelivery], ["vale", voucher(10)],]);
const choice = await ask("");rl.close();const method = methods.get(choice);if (method === undefined) { console.log("No existe esa forma de pago: " + choice);} else { const checkout = new Checkout(method); console.log(`${choice}: ${checkout.total(80).toFixed(2)} €`);}contra reembolsocontra reembolso: 82.50 €Una forma desconocida nunca llega al cobro:
PayPalNo existe esa forma de pago: PayPalLa elección sigue estando en un solo lugar, pero ahora son datos. Una forma nueva es una fila nueva en la tabla y una función nueva.
Más precisamente, para curiosos
¿Por qué una función y no una clase? En JavaScript una función es un valor: se guarda en un campo y en una tabla como cualquier otro objeto. Una estrategia con un método es exactamente una función. Una clase (o un objeto con métodos) compensa cuando la estrategia hace más de una cosa: por ejemplo un importe y una línea para la factura. En Java y C# lo mismo se escribe con una interfaz y una clase por forma.
Pruébalo
Predice la salida
El pedido es de 8 €, y el vale es de 10 €. Luego el cobro cambia a contra reembolso. ¿Qué imprime el programa?
const checkout = new Checkout(voucher(10));console.log(`${checkout.total(8).toFixed(2)} €`);
checkout.method = cashOnDelivery;console.log(`${checkout.total(8).toFixed(2)} €`);Pruébalo
Encuentra el error
Un vale debe usarse una vez. Alguien lo «gasta» dentro de la propia estrategia:
function voucher(value) { return (amount) => { const result = Math.max(amount - value, 0); value = 0; return result; };}const checkout = new Checkout(voucher(10));console.log(`Vista previa del pedido: ${checkout.total(80).toFixed(2)} €`);console.log(`Pago: ${checkout.total(80).toFixed(2)} €`);Vista previa del pedido: 70.00 €Pago: 80.00 €La vista previa mostró 70 €, y el pago pidió 80 €. ¿En qué línea está el error?
Elige la línea con el error.
El cobro llama a la estrategia cada vez que necesita el importe: para la vista previa, para la factura, para el pago. La estrategia solo debe calcular. En cuanto se cambia a sí misma, la segunda pregunta recibe una respuesta distinta. Quita la línea: el importe es de 70 € las dos veces. Si el vale se ha usado se anota en el pedido, después de que el pago se complete.
La idea
Cuándo no lo necesitas
Strategy tiene un coste: una función aparte por cada variante y una tabla, en lugar de un if. No compensa cuando:
- hay dos variantes y no cambian. «Con IVA o sin IVA» es un
if; - la regla se usa en un solo lugar y nadie la toca;
- solo hay una función, y la tabla está «por si acaso».
Compensa cuando las variantes crecen, cambian mientras el programa se ejecuta (elige el cliente), o distintas partes del programa necesitan reglas distintas.
Ejercicio
Apple Pay sin tocar el cobro
Resuelve la tarea en tu editor.
Apple Pay. La tienda añade Apple Pay con una comisión del 1 % sobre el importe. Añade una función applePay y paga con ella un pedido de 80 €. El cobro y las demás formas no se tocan.
Apple Pay: 80.80 €Más tareas
- Contra reembolso gratis sobre 100 €. Escribe una estrategia de contra reembolso que no cobre 2,50 € en los pedidos de más de 100 €. Ejecútala con 80 € y con 120 €.
- La forma desde la entrada. Añade la transferencia bancaria y el pago a plazos a la tabla de la tarjeta «Qué forma elige el cliente». Lee también el importe desde la entrada.
- Una tarjeta regalo con saldo. La tarjeta tiene un saldo de 50 €: paga lo que puede, y el resto lo paga la tarjeta bancaria. Piensa dónde se anota el saldo nuevo. ¿Lo cambia la estrategia, o el pedido después del pago?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Lanzar excepciones y crear las tuyas · Map y Set · Funciones flecha · Patrones de diseño
- Continúa en: Observer · Factory Method y Simple Factory · Singleton e inyección de dependencias · Adapter y Facade
- Ejercicios sobre el tema: Patrones de diseño