Ir al contenido

Taller asincrónico · Bloque E · segunda mitad de la sesión

Taller: Chain of Responsibility

Tres ejercicios para practicar, no para leer. Cada uno: diagnosticás el problema, lo resolvés en un editor PHP real, y recién después revelás la solución. El tercero es trampa — un caso donde CoR NO se aplica. Detectarlo es parte del ejercicio.

Esta es la segunda mitad de tu clase de Chain of Responsibility — no hay sesión en vivo de este tema. En la primera parte viste el patrón (problema, contrato, handlers, framework). Acá lo aplicás con tus manos. El miércoles 8 se integra con Decorator y Composite en el cierre del Bloque E — llegá con el ejercicio 2 funcionando.

Cómo se trabaja cada ejercicio

  1. Diagnosticá — escribí en el recuadro qué smell o patrón ves, antes de ver la respuesta. El nombre del patrón está oculto a propósito.
  2. Resolvé — abrí el código en php.cesar.sh (se abre con el código ya cargado), escribí tu solución y ejecutala de verdad. No te quedes en "me parece que da bien" — corré los casos.
  3. Compará — revelá el diagnóstico, la solución modelo y el “por qué”. ¿Acertaste el concepto?

¿Necesitás repaso? Volvé a la lectura de referencia: Chain of Responsibility. Este taller es insumo directo para la Mini-entrega 5 (Decorator + Composite + CoR).

Chain of Responsibility

Tres ejercicios: detectar la cascada de ifs acoplados, completar un handler y encadenarlo, y decidir cuándo CoR es sobre-ingeniería.

Ejercicio 1 ¿qué aplica acá?

PromoEngine decide qué descuento aplicar a un carrito. Cada regla se chequea en orden y, cuando una aplica, corta. Pero el orden y la lógica de cada regla viven juntos en un solo método: agregar una promo nueva, o cambiar la prioridad, obliga a abrir este método y reacomodar ifs.

Tu turno Diagnosticá: ¿qué problema de diseño tiene este método y qué patrón lo resuelve?
class PromoEngine
{
    public function apply(Cart $cart): Discount
    {
        // el orden ES la prioridad, y está hardcodeado acá
        if ($cart->hasCoupon()) {
            return new CouponDiscount($cart->coupon);
        }
        if ($cart->total() > 100) {
            return new BulkDiscount(0.10);
        }
        if ($cart->customer->isVip()) {
            return new VipDiscount(0.05);
        }
        // agregar "promo de temporada" = abrir esto y decidir
        // a mano en qué if va. reordenar = mover bloques y rezar.
        return new NoDiscount();
    }
}
Mostrar pista

¿Qué dos cosas distintas están mezcladas en apply()? Pensá en "qué decide cada regla" versus "en qué orden se prueban".

Ejercicio 2 ¿qué aplica acá?

Tenés el contrato base (OrderHandler) y dos handlers ya escritos: StockHandler y VendorHoursHandler. Falta el tercero, FraudHandler, que debe rechazar una orden si el total supera 500 y el pago no está verificado — y encadenarlo al final.

Tu turno Completá FraudHandler y agregalo a la cadena. Corré los 3 casos de prueba en PHP Scratch: 600 sin verificar (rechaza), 600 verificado (aprueba), 200 sin verificar (aprueba — la regla no dispara bajo 500).
abstract class OrderHandler
{
    private ?OrderHandler $next = null;
    public function setNext(OrderHandler $h): OrderHandler { $this->next = $h; return $h; }
    protected function passToNext(Order $order): void { $this->next?->handle($order); }
    abstract public function handle(Order $order): void;
}

class StockHandler extends OrderHandler
{
    public function __construct(private Inventory $inventory) {}
    public function handle(Order $order): void
    {
        if (!$this->inventory->hasStock($order)) throw new OutOfStockException();
        $this->passToNext($order);
    }
}

class VendorHoursHandler extends OrderHandler
{
    public function __construct(private VendorService $vendor) {}
    public function handle(Order $order): void
    {
        if (!$this->vendor->isOpen($order->vendorId)) throw new VendorClosedException();
        $this->passToNext($order);
    }
}

class FraudHandler extends OrderHandler
{
    public function __construct(private FraudService $fraud) {}
    // TODO: handle() — rechaza si total > 500 y el pago no está verificado

}

// TODO: armar la cadena stock → vendor → fraud, y correr los 3 casos
$stock->setNext($vendor);
// ...
Mostrar pista

El molde de un handler es siempre el mismo: chequeá tu condición, si falla lanzá la excepción, y SIEMPRE terminá en throw o en passToNext(). El error típico es olvidar passToNext cuando la condición no se cumple — ahí la cadena se corta en silencio y la orden nunca se aprueba.

Ejercicio 3 ¿qué aplica acá?

Un caso de uso valida un formulario de contacto: el nombre no puede estar vacío y el email debe tener formato válido. Son exactamente dos chequeos, fijos, que no van a cambiar. Alguien propone "usemos Chain of Responsibility para desacoplar las validaciones".

Tu turno Decidí: ¿CoR mejora este código, o es sobre-ingeniería? Justificá.
class ContactValidator
{
    public function validate(ContactForm $form): void
    {
        // dos validaciones fijas, siempre las mismas, en orden fijo
        if (trim($form->name) === '') {
            throw new ValidationException('El nombre es obligatorio.');
        }
        if (!filter_var($form->email, FILTER_VALIDATE_EMAIL)) {
            throw new ValidationException('El email no es válido.');
        }
    }
}
Mostrar pista

CoR paga cuando la cadena VARÍA (se agregan, quitan o reordenan pasos) o cuando cada paso necesita testearse por separado. ¿Algo de eso pasa acá?

Lo que sigue

Miércoles 8 · Cierre del Bloque E

Con esto cerrás Chain of Responsibility y, con él, el Bloque E — composición flexible. Los tres patrones del bloque comparten una idea: componer objetos en estructuras flexibles en vez de cablear comportamiento por herencia. Decorator apila capas sobre un objeto; Composite ramifica en árboles parte-todo; CoR encadena handlers donde cada uno procesa o pasa.

El miércoles los vemos coexistiendo en un mismo módulo del legacy. Llegá con tu FraudHandler del ejercicio 2 funcionando — es el punto de partida del cierre integrador.