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
- 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.
- 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.
- 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.
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.
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".
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
// El patrón es Chain of Responsibility: cada regla se vuelve un
// handler que decide si aplica (y corta) o pasa al siguiente.
abstract class PromoHandler
{
private ?PromoHandler $next = null;
public function setNext(PromoHandler $h): PromoHandler
{
$this->next = $h;
return $h; // fluent
}
protected function passToNext(Cart $cart): ?Discount
{
return $this->next?->handle($cart);
}
abstract public function handle(Cart $cart): ?Discount;
}
class CouponHandler extends PromoHandler
{
public function handle(Cart $cart): ?Discount
{
if ($cart->hasCoupon()) {
return new CouponDiscount($cart->coupon); // aplica y corta
}
return $this->passToNext($cart); // no aplica → pasa
}
}
// BulkHandler y VipHandler siguen el mismo molde.
// El orden (la prioridad) ahora es CONFIGURACIÓN, afuera:
$coupon->setNext($bulk)->setNext($vip);
$discount = $coupon->handle($cart) ?? new NoDiscount(); 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.
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.
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
class FraudHandler extends OrderHandler
{
public function __construct(private FraudService $fraud) {}
public function handle(Order $order): void
{
if ($order->total > 500 && !$this->fraud->isVerified($order)) {
throw new SuspectedFraudException();
}
$this->passToNext($order); // pasa, o es el último de la cadena
}
}
// armar la cadena y correr los casos
$stock->setNext($vendor)->setNext($fraud);
// caso 1: total 600, sin verificar → SuspectedFraudException
// caso 2: total 600, verificado → aprueba (llega al final)
// caso 3: total 200, sin verificar → aprueba (no dispara la regla)
$stock->handle($order); 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".
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á?
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
// Respuesta: NO. Acá CoR es sobre-ingeniería.
//
// CoR paga cuando la cadena varía, crece, o cada paso necesita
// ser testeable de forma independiente. Acá hay DOS validaciones
// fijas, en orden fijo, que nadie va a reordenar ni ampliar.
//
// Montar OrderHandler abstracto, dos handlers concretos, y armar
// la cadena para reemplazar dos ifs claros agrega indirección
// sin beneficio: el flujo se vuelve más difícil de seguir, no
// más fácil.
//
// El if/else directo es más honesto. Si MAÑANA aparecen 6
// validaciones que cambian por contexto (país, tipo de cuenta),
// AHÍ sí vale migrar a CoR. 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.