Ir al contenido
wannacode

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));
Salida
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 (IPaymentMethod en 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);
}
Tres formas, cada una en su propia clase. El vale tiene un campo con su valor, así que el valor llega en el constructor en lugar de ser un parámetro para todas:
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);
}
}
El cobro guarda la estrategia en un campo y calcula con ella. 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));
Salida
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
  1. El cobro recibe una tarjeta: su campo method apunta al objeto Card.
  2. total(80): el cobro no conoce la forma de pago. Solo le pregunta a la estrategia el importe y recibe 80.00.
  3. Una forma nueva. setMethod hace que el campo apunte al objeto CashOnDelivery. En Checkout no cambia ni una línea.
  4. El mismo total(80), otra respuesta: 82.50.
  5. Un vale de 10: un objeto con su propio campo value. Ya nada apunta a las estrategias antiguas.
  6. 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));
Salida
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
error: Installments is not abstract and does not override abstract method total(double) in PaymentMethod

Con 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));
}
Entrada
contra reembolso
Salida
contra reembolso: 82.50 €

Una forma desconocida nunca llega al cobro:

Entrada
PayPal
Salida
No existe esa forma de pago: PayPal

La 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));
Salida
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));
Respuesta

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));
Salida
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.

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.

Salida
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

1. La tienda añade Apple Pay. ¿Qué escribes con Strategy?
2. ¿Qué guarda el cobro con Strategy?
3. La estrategia del vale pone su valor a cero cada vez que se llama a total. El cobro pregunta una vez para la vista previa y otra para el pago. ¿Qué pasa?
4. ¿Cuándo no necesitas Strategy?

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