# Patrón Strategy en Java: Tu Pana Contra los if-else Desordenados.

12 min read
Patrón Strategy en Java: Tu Pana Contra los if-else Desordenados.
Tabla de Contenidos

Muchachones, aca les planteo una forma elegante de resolver ese problemita que todos cometemos en algun momento, asi que vamos a hablar de un dolor de cabeza que todos hemos sufrido: ese código que parece un plato de espagueti por culpa de una plaga de if-else-if-else…. Ya sabes de qué hablo, ese método que empezó con dos condiciones y ahora tiene veinte, y cada vez que lo tocas, rezas tres Padrenuestros para no romper otra cosa.!!!

El problema tiene nombre, y la solución también: el Patrón de Diseño Strategy. Y hoy, mi pana, te voy a enseñar a usarlo como un verdadero artesano del código. Mirando fuera de la caja!!!

Problema → Análisis → Solución Quirúrgica.!!! Así es como lo hacemos muchavhones!!!

1. El Problema: La Pesadilla de los if-else Anidados

Imagínate que estás montando el sistema de tu e-commerce, y te piden una funcionalidad “sencilla”: aplicar descuentos según el tipo de cliente.

Tu primer instinto, el de todos, es escribir algo así:

public class DiscountManager {
// El método de la discordia!!!
public double applyDiscount(User user, double amount) {
if ("GUEST".equals(user.getType())) {
// Los invitados no tienen descuento, que se afilien!!!
return amount;
} else if ("MEMBER".equals(user.getType())) {
// Al miembro fiel, su 10%
return amount * 0.90;
} else if ("PREMIUM".equals(user.getType())) {
// Al que paga más, se le da más: 20%
return amount * 0.80;
} else if ("VIP_GOLD".equals(user.getType())) {
// Llegó un nuevo tipo! El VIP Gold... 25% pa' él.
return amount * 0.75;
}
// ...y mañana te piden el "DIAMOND", el "PLATINUM"... NJDA!!!
return amount; // Por si acaso...
}
}

Al principio parece inofensivo, verdad?? jajaja!!! Eso es lo que el diablo quiere que pienses.

Qué está pasando realmente aquí?

  • Acumulación de basura: Cada nueva regla de negocio es otro else if. El método engorda más rápido que yo en diciembre.

  • Violación flagrante de SOLID: Especialmente el Principio Abierto/Cerrado (OCP). Esta vaina está CERRADA a la extensión y ABIERTA a la modificación. Todo al revés!!! Para añadir una nueva regla, tienes que modificar el código existente, lo cual en lo posible debemos evitar si o si.

  • Pruebas infernales: Para probar el descuento “VIP_GOLD”, tienes que pasar por toda la estructura condicional. No puedes probar una regla de forma aislada.

  • Alta complejidad ciclomática: Una forma elegante de decir que tu código tiene más caminos que un hormiguero y es un dolor de cabeza leerlo y mantenerlo.


2. Análisis Profundo: Qué Rayos Está Mal Aquí???

Vamos a ponernos serios y analizar por qué ese código es una bomba de tiempo.

Violación del Principio Abierto/Cerrado (OCP) Este es el pecado capital. OCP dice: “Tu código debe estar abierto para la extensión, pero cerrado para la modificación”.

  • Cerrado para modificación: Significa que una vez que una clase está probada y funcionando, no deberías tener que tocarla NUNCA MÁS para añadir nueva funcionalidad.

  • Abierto para extensión: Significa que deberías poder añadir nueva funcionalidad (como un nuevo tipo de descuento) creando código NUEVO, no editando el viejo (FUNDAMENTAL!!!).

Nuestro método applyDiscount se rie de este principio. Cada nuevo tipo de usuario te obliga a abrir el capó y meterle mano al motor. Riesgo de bugs por todos lados!!!

Analogía del Cirujano: Imagina que eres un cirujano. Para operar un nuevo paciente (añadir una feature), abres a todos los pacientes anteriores que ya estaban sanos y dados de alta? Claro que no! Trabajas en el nuevo paciente de forma aislada. El código debería ser igual papa…

Baja Cohesión y Alto Acoplamiento La clase DiscountManager se las sabe todas. Es una chismosa. Sabe la lógica de CADA tipo de descuento. Su responsabilidad es demasiado grande (violación del Principio de Responsabilidad Única - SRP). Debería haber un solo lugar que orqueste el descuento, no que calcule todos los descuentos.


3. Solución Quirúrgica: El Patrón Strategy al Rescate

Aquí es donde la magia ocurre, muchachones. La idea central del Patrón Strategy es tan simple como poderosa:

“Define una familia de algoritmos, encapsula cada uno en su propia clase y hazlos intercambiables entre sí.”

En criollo: en lugar de un if-else gigante, vamos a tener pequeñas “cajitas” (clases), cada una con una sola forma de calcular el descuento. Luego, elegimos la cajita que necesitamos en el momento.

Paso 1: Definir el Contrato (La Interfaz DiscountStrategy)

Primero, creamos un contrato. Una promesa. Le diremos al mundo: “Cualquier cosa que quiera ser una estrategia de descuento, DEBE saber cómo aplicar uno”. Esto lo hacemos con una interfaz.

// package com.cesarlead.strategy;
/**
* Este es el CONTRATO. La promesa sagrada.
* Toda estrategia de descuento DEBE implementar este método.
* No nos importa CÓMO lo haga, solo que lo haga.
*/
public interface DiscountStrategy {
/**
* Aplica la lógica de descuento específica de la estrategia.
* @param amount El monto original.
* @return El monto con el descuento ya aplicado.
*/
double applyDiscount(double amount);
}

Listo muchachones, acabamos de sentar las bases para cumplir con OCP. Estamos ready para extender esto con cuantas estrategias queramos.

Paso 2: Crear las Estrategias Concretas (Los Obreros Especialistas)

Ahora creamos nuestros “obreros”. Cada clase será un experto en UNA SOLA COSA. Una para los invitados, una para los miembros, etc.

  • Estrategia para Invitados (GuestDiscount)
// package com.cesarlead.strategy;
public class GuestDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double amount) {
System.out.println("Aplicando descuento de invitado: 0%");
// Para el invitado no hay nada, que pague completo! Jaja jaja
return amount;
}
}
  • Estrategia para Miembros (MemberDiscount)
// package com.cesarlead.strategy;
public class MemberDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double amount) {
System.out.println("Aplicando descuento de miembro fiel: 10%");
return amount * 0.90; // Toma tu 10%!!!
}
}
  • Estrategia para Miembros Premium (PremiumMemberDiscount)
// package com.cesarlead.strategy;
public class PremiumMemberDiscount implements DiscountStrategy {
@Override
public double applyDiscount(double amount) {
System.out.println("Aplicando descuento PREMIUM: 20%");
return amount * 0.80; // 20% por ser un patrón!!!
}
}

Ves la belleza de esto???

  • SRP en su máxima expresión: Cada clase tiene una, y solo UNA, razón para cambiar.

  • Fácil de probar: Puedes crear un test unitario solo para PremiumMemberDiscount sin que nada más te estorbe.

  • Legibilidad: El código es estúpidamente simple de leer.

Paso 3: Montar el Contexto (El Jefe que Delega)

Ahora necesitamos un “Contexto”. Esta es la clase que usará una de nuestras estrategias. Piénsalo como el jefe: él no hace el trabajo pesado, solo le dice al especialista “Hey, Bro!!! Haz lo tuyo!!!”.

// package com.cesarlead.strategy;
/**
* Este es el CONTEXTO. El jefe.
* No sabe CÓMO se calcula el descuento. Y no le importa!
* Solo sabe que tiene una 'strategy' que puede hacerlo.
*/
public class DiscountContext {
private DiscountStrategy strategy;
// Podemos cambiar la estrategia en cualquier momento con este método.
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}
// El jefe delega el trabajo pesado.
public double executeStrategy(double amount) {
if (strategy == null) {
// Por si se nos olvida poner una estrategia
return amount;
}
return strategy.applyDiscount(amount);
}
}

Este DiscountContext es un campeón de la delegación. Está totalmente desacoplado de la lógica concreta de los descuentos. Una maravilla muchachon!!!

Paso 4: El Cliente (Tú, eligiendo el poder)

Ahora, la parte más sabrosa. Veamos cómo el cliente (nuestro main, por ejemplo) usa todo este andamiaje.

// package com.cesarlead.strategy;
public class ECommerceApp {
public static void main(String[] args) {
double orderAmount = 250.0;
// Creamos nuestro "jefe" de descuentos
DiscountContext context = new DiscountContext();
// Escenario 1: Llega un invitado
System.out.println("--- Transacción de INVITADO ---");
context.setStrategy(new GuestDiscount());
double guestPrice = context.executeStrategy(orderAmount);
System.out.printf("El invitado paga: $%.2f%n%n", guestPrice);
// Escenario 2: Compra un miembro
System.out.println("--- Transacción de MIEMBRO ---");
context.setStrategy(new MemberDiscount());
double memberPrice = context.executeStrategy(orderAmount);
System.out.printf("El miembro paga: $%.2f%n%n", memberPrice);
// Escenario 3: Compra un Premium
System.out.println("--- Transacción de PREMIUM ---");
context.setStrategy(new PremiumMemberDiscount());
double premiumPrice = context.executeStrategy(orderAmount);
System.out.printf("El premium paga: $%.2f%n%n", premiumPrice);
}
}

EL MOMENTO DE LA REVELACIÓN!!!

El jefe te dice: “César, crea un nuevo descuento para Miembros VIP del 50%”.

Qué haces?, te pones a llorar y a editar ese if-else monstruoso??? NO, PAPA!!!

Haces esto:

  1. Creas UNA SOLA clase nueva:

    public class VipMemberDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
    System.out.println("Aplicando descuento VIP: 50% Toma, papa!");
    return amount * 0.50;
    }
    }
  2. La usas en el cliente:

    System.out.println("--- Transacción de VIP ---");
    context.setStrategy(new VipMemberDiscount()); // Y ya!!!
    double vipPrice = context.executeStrategy(orderAmount);
    System.out.printf("El VIP paga: $%.2f%n%n", vipPrice);

NO TOCASTE NINGUNA LÍNEA DE CÓDIGO EXISTENTE. Acabas de extender el sistema sin modificarlo. Acabas de aplicar OCP como un campeón!!!


4. Algunos diran:

  • Crítica 1: “Son demasiadas clases para un simple if!!!”

    “Mi pana, el software no es una foto, es una película papa. Hoy es un if simple, pero el negocio evoluciona. Este patrón no es para hoy, es para que tu ‘yo del futuro’ no te odie. Es pensar en escalabilidad y mantenibilidad.”

  • Crítica 2: “Los switch modernos con arrow syntax son más limpios.”

    “Tienes razón, la sintaxis es más sexy. case “MEMBER” -> … se ve bonito. Pero el problema de fondo sigue ahí: sigues violando OCP. Cada nuevo case es una modificación al bloque switch. Es maquillar el problema, no solucionarlo.”

  • Crítica 3: “Con Lambdas en Java 8+ esto es más fácil, no necesito clases.”

    “Totalmente de acuerdo!!! De hecho, las lambdas son la máxima expresión del Patrón Strategy para casos sencillos. Potencian el patrón!!!”

    Mira esta belleza:

    Map<String, Function<Double, Double>> strategies = Map.of(
    "GUEST", amount -> amount,
    "MEMBER", amount -> amount * 0.90,
    "PREMIUM", amount -> amount * 0.80
    );
    // Para usarlo:
    double finalPrice = strategies.get("MEMBER").apply(200.0); // Devuelve 180.0

    PERO, OJO AL CRISTO!!!

    Esta maravilla funciona perfecto cuando la lógica es stateless (sin estado) y simple. Pero qué pasa si tu estrategia PremiumMemberDiscount necesita llamar a una base de datos para verificar la antigüedad del miembro y aplicar un bono extra?, o si necesita usar otro servicio? Ahí, mi pana, una clase dedicada es infinitamente más limpia, organizada y testeable que una lambda gigantesca con dependencias inyectadas a la fuerza. Usa la herramienta correcta para el trabajo.


5. Implementación con Spring Boot

Si ya eres un samurái del código, seguramente usas Spring Boot. Spring lleva este patrón a otro nivel gracias a su * Inyección de Dependencias (DI)* y su contenedor de Inversión de Control (IoC). Agárrate bro!!!

Paso 1: Convierte tus estrategias en Beans de Spring

Le decimos a Spring: “Hey, gestiona estas clases por mí!!!”. Les damos un nombre para identificarlas.

@Component("GUEST") // El nombre del bean es "GUEST"
public class GuestDiscount implements DiscountStrategy { /* ... */
}
@Component("MEMBER")
public class MemberDiscount implements DiscountStrategy { /* ... */
}
@Component("PREMIUM")
public class PremiumMemberDiscount implements DiscountStrategy { /* ... */
}

Paso 2: El Servicio “Mágico” que Descubre las Estrategias

Este es el truco más cheverito de Spring. Vamos a crear un servicio que, al arrancar, le pide a Spring TODAS las implementaciones de DiscountStrategy y las guarda en un Map.

@Service
public class DiscountService {
// Spring inyectará un Map donde la clave es el nombre del bean (@Component)
// y el valor es la instancia de la estrategia. MAGIA!
private final Map<String, DiscountStrategy> strategies;
// El constructor recibe el Map mágico de Spring
public DiscountService(Map<String, DiscountStrategy> strategies) {
this.strategies = strategies;
}
public double calculateDiscountedPrice(String userType, double amount) {
// Obtenemos la estrategia del mapa por su nombre.
// Si no existe, usamos una por defecto (ej. GUEST).
DiscountStrategy strategy = strategies.getOrDefault(userType, strategies.get("GUEST"));
return strategy.applyDiscount(amount);
}
}

ANALIZA ESTA LOCURA!!!

Cuando mañana crees la clase VipMemberDiscount y la anotes con @Component(“VIP”), qué tienes que cambiar en * DiscountService*? ABSOLUTAMENTE NADA!!! Spring automáticamente la detectará, la meterá en el mapa y tu servicio funcionará sin que escribas una línea más. Coño, esto es poesía!!!

Paso 3: El Controlador REST para Exponerlo al Mundo

@RestController
@RequestMapping("/api/discounts")
public class DiscountController {
private final DiscountService discountService;
// Inyectamos nuestro servicio inteligente
public DiscountController(DiscountService discountService) {
this.discountService = discountService;
}
@GetMapping
public ResponseEntity<Double> getDiscount(
@RequestParam String userType,
@RequestParam double amount) {
double finalPrice = discountService.calculateDiscountedPrice(userType, amount);
return ResponseEntity.ok(finalPrice);
}
}

Ahora puedes llamar a GET /api/discounts?userType=PREMIUM&amount=500 y todo el flujo mágico se ejecutará.


6. Pruebas a Prueba de Balas (AssertJ + Mockito)

Código sin pruebas es como un político sin promesas: no sirve para nada jajaja.

6.1. Test Unitario de una Estrategia (Simple y Rápido)

import static org.assertj.core.api.Assertions.assertThat;
class PremiumMemberDiscountTest {
private final PremiumMemberDiscount discount = new PremiumMemberDiscount();
@Test
void givenAmount100_whenApplyDiscount_thenReturns80() {
// When
double result = discount.applyDiscount(100.0);
// Then
assertThat(result).isEqualTo(80.0);
}
}

6.2. Test Unitario del Servicio con Mocks (Probando al Jefe)

Aquí usamos Mockito para crear un “doble” de nuestras estrategias. Probamos que el DiscountService llama a la estrategia correcta, sin importarnos qué hace esa estrategia.

@ExtendWith(MockitoExtension.class)
class DiscountServiceTest {
@Mock
private Map<String, DiscountStrategy> strategyMap;
@Mock
private DiscountStrategy premiumStrategy;
@Mock
private DiscountStrategy guestStrategy;
private DiscountService discountService;
@BeforeEach
void setUp() {
// Le enseñamos a nuestro mapa falso cómo comportarse
when(strategyMap.get("PREMIUM")).thenReturn(premiumStrategy);
when(strategyMap.get("GUEST")).thenReturn(guestStrategy);
when(strategyMap.getOrDefault(eq("NON_EXISTENT"), any())).thenReturn(guestStrategy);
discountService = new DiscountService(strategyMap);
}
@Test
void givenPremiumUser_whenCalculate_thenPremiumStrategyIsCalled() {
// Given
when(premiumStrategy.applyDiscount(200.0)).thenReturn(160.0);
// When
double result = discountService.calculateDiscountedPrice("PREMIUM", 200.0);
// Then
assertThat(result).isEqualTo(160.0);
verify(premiumStrategy).applyDiscount(200.0); // Verificamos que se llamó al método correcto
verify(guestStrategy, never()).applyDiscount(anyDouble()); // Y que el otro no se tocó
}
}

Muchachones, si llegaron hasta aquí, se merecen una cerveza bien fría. Ya no tienen excusa para escribir código con * if-else* que parece un nido de ratas.

Puntos Clave para que te los tatúes en el cerebro mi bro:

  1. Detecta el Olor: Cuando veas un if-else-if o un switch basado en un “tipo” que controla un comportamiento, suenan las alarmas del Patrón Strategy!!!

  2. Abraza OCP: Tu meta es extender sin modificar. El Strategy es tu mejor pana para lograrlo.

  3. Delega, No Acumules: Crea clases pequeñas y enfocadas (SRP). Una clase que orquesta (Contexto) y muchas que ejecutan (Estrategias).

  4. Usa Lambdas para lo Simple: Si la lógica es sencilla y sin estado, un Map de lambdas es elegante y conciso.

  5. Domina Spring Boot: Deja que Spring descubra y te inyecte las estrategias. Es automático, limpio y te hace ver como un pro.

  6. Prueba como un Demente: Prueba cada pieza por separado (unit tests) y luego todo el conjunto (integration tests).

“No será mejor una estrategia modular, elegante y a prueba de futuro?”

Ya tienen su kit quirúrgico, muchachones!!! Salgan a refactorizar ese código espagueti y a construir software del que se sientan orgullosos. Activitos!!!

Foto Cesar Fernandez

¿Lo rompiste? ¿Lo mejoraste?

Gracias por llegar hasta el final. Escribo estos posts para organizar mis propias ideas y, con suerte, para ahorrarle a alguien más el dolor de cabeza que yo ya pasé. Me encuentras en LinkedIn o puedes ver más de mi trabajo en GitHub.


Más Artículos