Práctica asincrónica · S5 V · viernes 19 de junio
Práctica: Singleton + Observer
Seis ejercicios para practicar, no para leer. Cada uno: diagnosticás el problema, lo refactorizás en un editor PHP real, y recién después revelás la solución. Dos de ellos son trampa — casos donde el patrón NO se aplica. Detectarlos es parte del ejercicio.
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.
- Refactorizá — abrí el código en php.cesar.sh, escribí tu solución y ejecutala de verdad.
- Compará — revelá el diagnóstico, la solución modelo y el “por qué”. ¿Acertaste el concepto?
¿Necesitás repaso? Tenés las lecturas de referencia: Singleton y Observer. Esta práctica es el insumo directo para la Mini-entrega 3.
Singleton
Tres ejercicios: detectar la dependencia oculta, conservar la única instancia sin acceso global, y decidir cuándo un Singleton no es un problema.
ReportMailer envía un reporte. Su firma dice que solo necesita un destinatario. Pero por dentro alcanza un Logger global. La firma miente sobre lo que de verdad hace falta.
class ReportMailer
{
public function __construct(private string $to) {}
public function send(string $body): void
{
// la firma dijo "solo necesito un destinatario"...
Logger::getInstance()->log("Enviando reporte a {$this->to}");
// ...pero send() necesita un Logger global. Nadie lo declaró.
mail($this->to, 'Reporte', $body);
}
}
// Quien lo usa cree que basta con el destinatario:
$mailer = new ReportMailer('jefe@empresa.sv');
$mailer->send('ventas del mes'); Mostrar pista
¿Qué necesitás para escribir un test de send() sin mandar un correo de verdad ni depender del Logger global?
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
// La dependencia entra por el constructor: la firma ya no miente.
class ReportMailer
{
public function __construct(
private string $to,
private Logger $logger,
) {}
public function send(string $body): void
{
$this->logger->log("Enviando reporte a {$this->to}");
mail($this->to, 'Reporte', $body);
}
}
// En un test, pasás un logger falso sin pelear con nada:
$mailer = new ReportMailer('jefe@empresa.sv', new FakeLogger());
$mailer->send('ventas del mes'); El equipo quiere que haya UNA sola conexión a la base en toda la app. Alguien lo resolvió con un Singleton clásico. Funciona, pero todo el código alcanza Database::getInstance() por su cuenta.
class Database
{
private static ?Database $instance = null;
private function __construct() {}
public static function getInstance(): static
{
return self::$instance ??= new self();
}
public function query(string $sql): array { /* ... */ return []; }
}
// En cualquier parte del código, sin pedírselo a nadie:
$rows = Database::getInstance()->query('SELECT * FROM orders'); Mostrar pista
¿Quién debería garantizar la unicidad: la clase misma, o el armado de la app? Hevery distingue "Singleton" (GoF) de "una sola instancia".
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
// Clase normal: constructor público, sin getInstance() global.
class Database
{
public function __construct() { /* abre conexión */ }
public function query(string $sql): array { /* ... */ return []; }
}
// La unicidad la decide el contenedor, NO la clase:
// (Laravel) en un Service Provider:
$this->app->singleton(Database::class);
// y quien la necesita la RECIBE, no la busca:
class OrderRepository
{
public function __construct(private Database $db) {}
} Un contador de IDs en memoria para un script de migración de un solo uso, que corre una vez de principio a fin y se descarta. Alguien propone "quitarle el Singleton porque Hevery dijo que son malos".
// Script de migración de un solo uso (corre una vez, no se testea unitariamente)
class IdSequence
{
private static ?IdSequence $instance = null;
private int $next = 1;
private function __construct() {}
public static function getInstance(): static
{
return self::$instance ??= new self();
}
public function nextId(): int { return $this->next++; }
}
foreach ($legacyRows as $row) {
$row['id'] = IdSequence::getInstance()->nextId();
insert($row);
} Mostrar pista
La crítica de Hevery es práctica: el Singleton vuelve el código difícil de TESTEAR y de CAMBIAR. ¿Aplica ese costo a un script de un solo uso?
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
// Respuesta: NO hace falta refactorizar. El Singleton acá es aceptable.
//
// Razón: el costo que Hevery señala (testing frágil, dependencias
// ocultas que estorban al evolucionar) no se paga en un script
// de un solo uso, sin tests unitarios, que corre una vez y muere.
//
// Aplicar DI + contenedor acá sería sobre-ingeniería: agregar
// estructura para resolver un problema que este código no tiene.
//
// (Si MAÑANA este IdSequence se moviera a la app de producción,
// que sí se testea y evoluciona, AHÍ sí valdría refactorizar.) Observer
Tres ejercicios: detectar el método que crece, completar el sujeto que notifica, y decidir cuándo Observer es sobre-ingeniería.
StockManager descuenta inventario y, en el mismo método, avisa a mano a cada sistema que tiene que reaccionar: caché, dashboard, reabastecimiento. Cada sistema nuevo es otra línea acá adentro.
class StockManager
{
public function reduce(Product $product, int $qty): void
{
$product->stock -= $qty;
$product->save();
// avisa a mano a cada reaccionador
(new CacheService())->invalidate($product);
(new MetricsDashboard())->record($product, $qty);
(new ReorderService())->checkThreshold($product);
// ... y cada sistema nuevo agrega otra línea acá ...
}
} Mostrar pista
Hacé la pregunta: ¿qué debería saber StockManager sobre QUIÉN reacciona cuando baja el stock?
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
interface StockObserver
{
public function onStockReduced(Product $product, int $qty): void;
}
class StockManager
{
private array $observers = [];
public function subscribe(StockObserver $o): void
{
$this->observers[] = $o;
}
public function reduce(Product $product, int $qty): void
{
$product->stock -= $qty;
$product->save();
// ya no instancia a nadie: solo avisa por la interfaz
foreach ($this->observers as $o) {
$o->onStockReduced($product, $qty);
}
}
}
// el wiring vive AFUERA. Agregar un reaccionador = una línea, sin tocar StockManager:
$mgr = new StockManager();
$mgr->subscribe(new CacheObserver());
$mgr->subscribe(new ReorderObserver()); Tenés la interfaz y dos observadores ya escritos. Falta el sujeto: AuctionHouse debe poder suscribir pujadores y avisarles a todos cuando entra una oferta más alta.
interface BidObserver
{
public function onNewBid(float $amount): void;
}
class EmailBidder implements BidObserver
{
public function onNewBid(float $amount): void
{
echo "Email: nueva puja de \$$amount\n";
}
}
class DashboardBidder implements BidObserver
{
public function onNewBid(float $amount): void
{
echo "Dashboard actualizado: \$$amount\n";
}
}
class AuctionHouse
{
// TODO: lista de observadores
// TODO: subscribe(BidObserver $o)
public function placeBid(float $amount): void
{
echo "Puja registrada: \$$amount\n";
// TODO: avisar a todos los observadores
}
}
// debe funcionar así:
$auction = new AuctionHouse();
$auction->subscribe(new EmailBidder());
$auction->subscribe(new DashboardBidder());
$auction->placeBid(150.00); Mostrar pista
Las tres piezas del sujeto: una propiedad array, un método que agrega a esa lista, y un foreach en placeBid().
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
class AuctionHouse
{
private array $observers = [];
public function subscribe(BidObserver $o): void
{
$this->observers[] = $o;
}
public function placeBid(float $amount): void
{
echo "Puja registrada: \$$amount\n";
foreach ($this->observers as $o) {
$o->onNewBid($amount);
}
}
}
// salida esperada:
// Puja registrada: $150
// Email: nueva puja de $150
// Dashboard actualizado: $150 Un caso de uso registra un pago y, al terminar, manda exactamente un correo de confirmación. Siempre uno, nunca más. Alguien propone "metamos Observer para desacoplar el envío del correo".
class ConfirmPayment
{
public function __construct(private Mailer $mailer) {}
public function handle(Payment $payment): void
{
$payment->markAsPaid();
$payment->save();
// un único efecto, fijo, que no va a multiplicarse
$this->mailer->sendConfirmation($payment);
}
} Mostrar pista
Observer resuelve uno-a-MUCHOS y reaccionadores que van y vienen. ¿Cuántos reaccionadores hay acá, y van a cambiar?
Compará tu diagnóstico del Paso 1 con esto. ¿Acertaste el concepto?
Ver solución modelo en código
// Respuesta: NO. Acá Observer es sobre-ingeniería.
//
// Observer paga cuando hay VARIOS reaccionadores, o cuando
// entran y salen sin que el emisor deba enterarse. Acá hay
// UNO solo, fijo: mandar la confirmación. Siempre uno.
//
// Meter una interfaz, una lista de observers y subscribe()
// para un único efecto agrega indirección sin beneficio:
// el flujo se vuelve más difícil de seguir, no más fácil.
//
// La llamada directa ($this->mailer->sendConfirmation())
// es más honesta: se lee de un vistazo qué pasa al confirmar.
//
// (Si mañana hubiera que avisar a 4 sistemas distintos y la
// lista creciera, AHÍ recién Observer empezaría a pagar.) Lo que sigue
Mini-entrega 3
Ya practicaste los tres patrones del Bloque B+C en dominios sintéticos. Ahora aplicalos sobre el legacy real: Factory Method, Singleton (tu decisión justificada: Singleton / DI / Service Container) y Observer. Vos elegís dónde encaja cada uno — detectarlo es parte del trabajo. Con bitácora de 5 preguntas.
Cierra jueves 25 de junio, 23:59 · 7% · branch me3 + PR a main + bitácora