Strategy
En resumen: Strategy pone cada regla (por ejemplo una forma de pago) en su propio objeto detrás de una interfaz comú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í:
static double total(String method, double amount) { if (method.equals("tarjeta")) { return amount; } else if (method.equals("contra reembolso")) { return amount + 2.50; } else if (method.equals("vale")) { return Math.max(amount - 10, 0); } else if (method.equals("transferencia bancaria")) { return amount * 0.98; } else if (method.equals("pago a plazos")) { return amount * 1.05; } throw new IllegalArgumentException("Forma de pago desconocida: " + method);}System.out.printf("Tarjeta: %.2f €%n", total("tarjeta", 80));System.out.printf("Contra reembolso: %.2f €%n", total("contra reembolso", 80));System.out.printf("Vale: %.2f €%n", total("vale", 80));System.out.printf("Transferencia bancaria: %.2f €%n", total("transferencia bancaria", 80));System.out.printf("Pago a plazos: %.2f €%n", total("pago a plazos", 80));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 este método. 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, el método 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 un objeto aparte. Todas las formas comparten una forma común: una interfaz con un método que recibe un importe y devuelve cuánto hay que pagar. El cobro guarda una referencia a un objeto así y llama al método sin saber de qué objeto 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: la interfaz
PaymentMethod(IPaymentMethoden C#); - las estrategias concretas: una clase para cada forma.
Una forma nueva es una estrategia nueva. El cobro no se toca.
Míralo
El cobro y tres formas de pago
La forma común:
interface PaymentMethod { double total(double amount);}class Card implements PaymentMethod { @Override public double total(double amount) { return amount; }}
class CashOnDelivery implements PaymentMethod { @Override public double total(double amount) { return amount + 2.50; }}
class Voucher implements PaymentMethod { private double value;
public Voucher(double value) { this.value = value; }
@Override public double total(double amount) { return Math.max(amount - value, 0); }}setMethod la cambia:class Checkout { private PaymentMethod method;
public Checkout(PaymentMethod method) { this.method = method; }
public void setMethod(PaymentMethod method) { this.method = method; }
public double total(double amount) { return Math.round(method.total(amount) * 100) / 100.0; }}Checkout checkout = new Checkout(new Card());System.out.printf("Tarjeta: %.2f €%n", checkout.total(80));
checkout.setMethod(new CashOnDelivery());System.out.printf("Contra reembolso: %.2f €%n", checkout.total(80));
checkout.setMethod(new Voucher(10));System.out.printf("Vale: %.2f €%n", checkout.total(80));Tarjeta: 80.00 €Contra reembolso: 82.50 €Vale: 70.00 €Checkout checkout = new Checkout(new Card());System.out.printf("Tarjeta: %.2f €%n", checkout.total(80));
checkout.setMethod(new CashOnDelivery());System.out.printf("Contra reembolso: %.2f €%n", checkout.total(80));
checkout.setMethod(new Voucher(10));System.out.printf("Vale: %.2f €%n", checkout.total(80));Paso a paso, en palabras
- El cobro recibe una tarjeta: su campo method apunta al objeto 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. setMethod hace que el campo apunte al objeto CashOnDelivery. En Checkout no cambia ni una línea.
- El mismo total(80), otra respuesta: 82.50.
- Un vale de 10: un objeto con su propio campo value. Ya nada apunta a las estrategias 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 clases nuevas:
class BankTransfer implements PaymentMethod { @Override public double total(double amount) { return amount * 0.98; }}
class Installments implements PaymentMethod { @Override public double total(double amount) { return amount * 1.05; }}Checkout checkout = new Checkout(new BankTransfer());System.out.printf("Transferencia bancaria: %.2f €%n", checkout.total(80));
checkout.setMethod(new Installments());System.out.printf("Pago a plazos: %.2f €%n", checkout.total(80));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.
Hay algo más. Si la clase nueva olvida el método o escribe mal su nombre, el programa no compila:
class Installments implements PaymentMethod { public double totl(double amount) { return amount * 1.05; }}error: Installments is not abstract and does not override abstract method total(double) in PaymentMethodCon la cadena de if, un nombre de método mal escrito solo se nota cuando un cliente elige esa forma.
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.
Map<String, PaymentMethod> methods = Map.of( "tarjeta", new Card(), "contra reembolso", new CashOnDelivery(), "vale", new Voucher(10));
Scanner scanner = new Scanner(System.in);String choice = scanner.nextLine();PaymentMethod method = methods.get(choice);if (method == null) { System.out.println("No existe esa forma de pago: " + choice);} else { Checkout checkout = new Checkout(method); System.out.printf("%s: %.2f €%n", choice, checkout.total(80));}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 clase nueva.
Más precisamente, para curiosos
PaymentMethod tiene un método, así que es una interfaz funcional, y una estrategia también puede ser una lambda:
Checkout checkout = new Checkout(amount -> amount + 2.50);System.out.printf("Contra reembolso: %.2f €%n", checkout.total(80));Contra reembolso: 82.50 €Una lambda basta para una regla de una línea. Una clase compensa cuando la estrategia tiene un campo propio (el vale), un nombre o más de un método.
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?
Checkout checkout = new Checkout(new Voucher(10));System.out.printf("%.2f €%n", checkout.total(8));
checkout.setMethod(new CashOnDelivery());System.out.printf("%.2f €%n", checkout.total(8));Pruébalo
Encuentra el error
Un vale debe usarse una vez. Alguien lo «gasta» dentro de la propia estrategia:
class Voucher implements PaymentMethod { private double value;
public Voucher(double value) { this.value = value; }
@Override public double total(double amount) { double result = Math.max(amount - value, 0); value = 0; return result; }}Checkout checkout = new Checkout(new Voucher(10));System.out.printf("Vista previa del pedido: %.2f €%n", checkout.total(80));System.out.printf("Pago: %.2f €%n", checkout.total(80));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 interfaz y una clase por cada variante, 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 estrategia, «por si acaso». Una interfaz con una sola implementación es código que tienes que leer sin recibir nada a cambio.
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 clase 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 · HashMap y HashSet · Lambdas · 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