Ir al contenido

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

  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. Refactorizá — abrí el código en php.cesar.sh, escribí tu solución y ejecutala de verdad.
  3. 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.

Ejercicio 1 ¿qué aplica acá?

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.

Tu turno Diagnosticá: ¿qué problema de diseño tiene esta clase y por qué duele en un test?
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?

Ejercicio 2 ¿qué aplica acá?

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.

Tu turno Querés conservar la única instancia, pero sin que la clase se autogestione ni se exponga global. ¿Cómo lo reescribís?
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".

Ejercicio 3 ¿qué aplica acá?

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".

Tu turno Decidí: ¿este Singleton es un problema que hay que refactorizar, o acá está bien? Justificá.
// 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?

Observer

Tres ejercicios: detectar el método que crece, completar el sujeto que notifica, y decidir cuándo Observer es sobre-ingeniería.

Ejercicio 4 ¿qué aplica acá?

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.

Tu turno Diagnosticá qué patrón aplica y por qué este método nunca va a dejar de crecer.
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?

Ejercicio 5 ¿qué aplica acá?

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.

Tu turno Completá AuctionHouse: una lista de observadores, subscribe(), y placeBid() que notifique a todos. (Probalo en PHP Scratch.)
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().

Ejercicio 6 ¿qué aplica acá?

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".

Tu turno Decidí: ¿Observer mejora este código, o es sobre-ingeniería? Justificá.
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?

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

Ver la consigna completa en Moodle →