Ir al contenido

Repaso · POO

Programación orientada a objetos

Cuatro pilares · tres conceptos clave · ~20 minutos de lectura

¿Por qué este repaso?

Los principios SOLID y los patrones GoF se apoyan en la misma base: programación orientada a objetos. Si esa base está borrosa, las reglas de SOLID se sienten arbitrarias y los patrones se vuelven recetas que no se entienden por dentro.

Esta guía no enseña POO desde cero. Asume que ya la vieron en una materia previa y la usa de espejo: para cada concepto, muestra un ejemplo en cuatro lenguajes, explica el refactor mínimo y conecta el concepto con el principio SOLID o el patrón donde reaparece.

Cómo usar esta guía

Si algo de SOLID o un patrón no te cuadra, regresá al pilar o concepto correspondiente. La tabla del cierre te da el atajo.

Tip de lectura: Tocá, enfocá o pasá el mouse sobre las líneas marcadas para ver la anotación con el por qué.

Clase vs objeto

Una clase es la plantilla: dice qué datos tiene y qué puede hacer. Un objeto es una instancia concreta de esa plantilla, con valores propios y vida propia. La clase se escribe una vez; los objetos se crean muchas veces y conviven al mismo tiempo.

La distinción importa porque casi todo lo demás depende de ella: cuando hablemos de estado, hablamos del objeto; cuando hablemos de contrato o tipo, hablamos de la clase (o la interfaz).

Pilar 1 — Encapsulamiento

Encapsulation

Sin encapsulamiento, las reglas que protegen tus datos viven en quien los usa, no en quien los tiene. Se duplican, se olvidan, y un día alguien deja un saldo en negativo o un email vacío en la base de datos.

El objeto guarda su propio estado y decide cómo se modifica. Los datos no son de acceso libre: están encapsulados dentro del objeto, y solo se tocan por los métodos que él expone. Ahí viven también las reglas que protegen ese estado.

Una analogía: el cajero automático. No metés la mano en la bóveda; le decís "quiero retirar 100" y el cajero verifica las reglas (¿hay saldo?, ¿está autenticado?) antes de moverse. La bóveda es privada; el cajero es la interfaz pública que protege el contenido.

Antes

// Saldo expuesto: cualquiera lo modifica sin reglas.
class Cuenta
{
    public float $saldo = 0;
}

// En otro archivo, lejos de la clase:
$cuenta->saldo -= 500; // nadie valida

Después

class Cuenta
{
    private float $saldo = 0;

    public function depositar(float $monto): void
    {
        if ($monto <= 0) throw new \InvalidArgumentException('monto inválido');
        $this->saldo += $monto;
    }

    public function retirar(float $monto): void
    {
        if ($monto > $this->saldo) throw new \DomainException('saldo insuficiente');
        $this->saldo -= $monto;
    }
}
¿Dónde aparece en SOLID / patrones?

SRP: al meter las reglas del saldo dentro de Cuenta, esa clase tiene una sola razón para cambiar — las reglas del saldo. Si las reglas estuvieran dispersas en quien usa la cuenta, varios lugares tendrían razón para cambiar cuando la regla cambie. Encapsular es la forma más directa de respetar SRP a nivel de datos.

¿Detectaste el smell?

En el "antes", la regla "el saldo nunca debe ser negativo" aparece como decisión del negocio. ¿Qué hay que hacer para que esa regla se cumpla siempre, sin depender de la memoria del programador?

Pilar 2 — Abstracción

Abstraction

Sin abstracción, cada parte de tu sistema termina enganchada al detalle concreto del proveedor que usa hoy. Cambiar de Stripe a PayPal, o de MySQL a Postgres, deja de ser cuestión de cambiar una pieza y se convierte en buscar todos los lugares que dependen de la pieza vieja.

Una abstracción dice qué hace algo sin obligarte a saber cómo. Cuando llamás a un método, idealmente sabés qué pedir y qué esperar; el funcionamiento interno está oculto detrás de ese contrato.

En código, las abstracciones aparecen casi siempre como interfaces (o clases abstractas): el contrato vive ahí, las implementaciones concretas viven en otras clases que cumplen ese contrato.

Antes

// El servicio depende del cliente concreto de Stripe.
class ServicioPagos
{
    public function __construct(private StripeClient $stripe) {}

    public function procesar(int $monto): void
    {
        $this->stripe->charge($monto);
    }
}

Después

interface PasarelaPago
{
    public function cobrar(int $monto): void;
}

class ServicioPagos
{
    public function __construct(private PasarelaPago $pasarela) {}

    public function procesar(int $monto): void
    {
        $this->pasarela->cobrar($monto);
    }
}
¿Dónde aparece en SOLID / patrones?

DIP: el módulo de alto nivel (ServicioPagos) deja de depender del detalle (StripeClient) y pasa a depender de una abstracción (PasarelaPago). El detalle también depende de la abstracción (la implementa). Ambos miran al mismo contrato. Esto también es la base de patrones como Strategy.

¿Detectaste el smell?

¿Cuál es la diferencia práctica más importante entre el "antes" y el "después"?

Pilar 3 — Herencia

Inheritance

Sin herencia, todo lo común entre clases parecidas se duplica. Repetís código, repetís bugs, y un cambio de regla obliga a tocar varios lugares.

La herencia permite que una clase reutilice y extienda otra: la subclase recibe los campos y métodos de la superclase y puede agregar o redefinir comportamiento. Sirve cuando la subclase respeta y extiende el contrato de la superclase (ej. Gerente extends Empleado sumando un bono: sigue siendo un empleado, solo cobra distinto).

Una analogía: heredar es como decir "soy un tipo de…". Un gerente es un tipo de empleado funciona — el gerente cumple todo lo que se espera de un empleado y además tiene cosas propias. Un perro es un tipo de mueble porque ambos tienen cuatro patas no funciona — el perro no cumple el contrato de mueble (no podés apoyar un florero encima sin sorpresas).

Es una herramienta poderosa y traicionera. Heredar por parecido superficial ("un contratista es como un empleado") suele terminar mal: la subclase rompe el contrato de la superclase, y los clientes que esperaban un Empleado se llevan una sorpresa.

Antes

class Empleado
{
    public function calcularSalario(): float
    {
        return 1000;
    }
}

class Contratista extends Empleado
{
    public function calcularSalario(): float
    {
        throw new \LogicException('los contratistas facturan, no tienen salario');
    }
}

Después

interface PagoMensual
{
    public function montoDelMes(): float;
}

class Empleado implements PagoMensual
{
    public function montoDelMes(): float { return 1000; }
}

class Contratista implements PagoMensual
{
    public function montoDelMes(): float { return $this->facturaDelMes(); }
    private function facturaDelMes(): float { return 1500; }
}
¿Dónde aparece en SOLID / patrones?

LSP: una subclase debe poder usarse donde se usa la superclase sin romper expectativas. Si la subclase lanza una excepción donde la superclase devuelve un valor, la herencia está mal puesta. La regla práctica: heredá cuando la subclase respete y extienda el contrato; usá un contrato común (interfaz) cuando dos clases solo comparten un comportamiento, no una identidad. En el "después" elegimos lo segundo porque Contratista no es un empleado, solo comparte la idea de cobrar a fin de mes.

¿Detectaste el smell?

En el "antes", una función nominaTotal(Empleado[] empleados) recibe una lista que incluye un Contratista. ¿Qué pasa al iterar y llamar calcularSalario()?

Pilar 4 — Polimorfismo

Polymorphism

Sin polimorfismo, cada vez que aparece un caso nuevo tenés que ir a buscar todos los switch / if/else if que decidían por tipo y agregar una rama en cada uno. Si olvidás uno, hay un bug silencioso.

Polimorfismo es la idea de que el mismo mensaje produce comportamientos distintos según el objeto que lo recibe. Una figura sabe calcular su propia área: un círculo lo hace con π·r², un cuadrado con lado², un triángulo con base·altura/2. Quien usa figuras no necesita saber qué tipo es cada una — solo le pide area().

Una analogía: el control remoto universal. Apretás "play" y la tele empieza a reproducir, el equipo de sonido también, el Blu-ray también — el mismo botón, comportamientos distintos. El control no sabe cómo reproduce cada aparato; cada uno responde a su manera al mismo mensaje.

Es lo que permite eliminar los switch por tipo: el dispatch lo hace el lenguaje, no tu código.

Antes

function areaDe(object $figura): float
{
    switch (get_class($figura)) {
        case 'Circulo':   return M_PI * $figura->radio ** 2;
        case 'Cuadrado':  return $figura->lado ** 2;
        case 'Triangulo': return $figura->base * $figura->altura / 2;
    }
    throw new \InvalidArgumentException('figura desconocida');
}

Después

interface Figura { public function area(): float; }

class Circulo   implements Figura {
    public function __construct(public float $radio) {}
    public function area(): float { return M_PI * $this->radio ** 2; }
}
class Cuadrado  implements Figura {
    public function __construct(public float $lado) {}
    public function area(): float { return $this->lado ** 2; }
}

function areaDe(Figura $f): float { return $f->area(); }
¿Dónde aparece en SOLID / patrones?

OCP: abierto a extensión (agregar una nueva clase que implemente Figura), cerrado a modificación (la función areaDe no se toca). El polimorfismo es el mecanismo de POO que permite OCP. Cuando veas un switch por tipo, casi siempre hay un OCP esperando que lo respeten con polimorfismo. Es la base del patrón Strategy.

¿Detectaste el smell?

Aparece una Estrella de cinco puntas. En el "antes" hay que tocar la función areaDe para agregar el caso. ¿Por qué eso es señal de problema?

Interfaz vs clase abstracta

Interface vs Abstract Class

Una interfaz es un contrato puro: dice qué métodos existen, sin código. Una clase abstracta es una clase a medio terminar: puede tener métodos ya implementados y otros marcados como abstractos, y se usa para compartir implementación entre subclases.

Regla práctica: si solo querés definir un contrato, usá interfaz. Si además tenés código común que varias clases comparten, una clase abstracta es una opción. Y mantené las interfaces pequeñas: una interfaz gorda obliga a sus implementadores a cargar métodos que no usan.

Antes

interface Reporte
{
    public function aPdf(): string;
    public function aCsv(): string;
    public function enviarPorEmail(): void;
    public function enviarPorSftp(): void;
    public function registrarAuditoria(): void;
}

Después

interface Formato    { public function aPdf(): string; }
interface Enviable   { public function enviar(): void; }
interface Auditable  { public function registrarAuditoria(): void; }

class ReporteVentas implements Formato, Auditable
{
    public function aPdf(): string { return '…pdf…'; }
    public function registrarAuditoria(): void { /* … */ }
}
¿Dónde aparece en SOLID / patrones?

ISP: los clientes no deben depender de métodos que no usan. Una interfaz "gorda" obliga a todos sus implementadores a cargar con métodos ajenos; partirla en interfaces chicas y enfocadas mantiene a cada clase honesta sobre lo que realmente cumple.

¿Detectaste el smell?

¿Cuándo es preferible una clase abstracta sobre una interfaz?

Composición sobre herencia

Composition over Inheritance

Cuando una clase necesita una capacidad nueva, la salida más rápida parece ser heredar de otra que ya la tenga. El problema es que cada eje independiente de variación se vuelve un nivel de la jerarquía, y la jerarquía estalla.

Componer significa tener en lugar de ser: la clase guarda un colaborador y le delega. Un personaje no es "con vida y con ataque": tiene una Vida y un Ataque, y se los podés cambiar en tiempo de ejecución.

Antes

class Personaje {}
class PersonajeConVida extends Personaje { /* hp, recibirDanio */ }
class PersonajeConVidaYAtaqueMelee extends PersonajeConVida { /* atacar */ }
class PersonajeConVidaYAtaqueMagico extends PersonajeConVida { /* hechizar */ }
class PersonajeConVidaAtaqueMeleeYInventario extends PersonajeConVidaYAtaqueMelee { /* … */ }

Después

interface Ataque { public function golpear(Personaje $objetivo): void; }

class Personaje
{
    public function __construct(
        private Vida $vida,
        private Ataque $ataque,
    ) {}

    public function atacar(Personaje $otro): void { $this->ataque->golpear($otro); }
}
¿Dónde aparece en SOLID / patrones?

Patrones: esto es la base de varios. Strategy inyecta el algoritmo en lugar de heredarlo (el Ataque es la estrategia). Decorator envuelve un objeto con otro que añade comportamiento sin tocar al original. Cuando veas "preferir composición sobre herencia" repetido en SOLID y en patrones, viene de acá.

¿Detectaste el smell?

En el "antes", un personaje puede cambiar su forma de atacar (de melee a mágico) en mitad del juego. ¿Qué hay que hacer?

Programar contra interfaces

Program to an Interface

Esto es primo cercano de "composición sobre herencia", pero responde a otra pregunta. Composición se trata de cómo está construida una clase (tener un colaborador en lugar de heredar de él). Programar contra interfaces se trata del tipo declarado de ese colaborador: si lo declarás como una clase concreta o como un contrato.

En la práctica: si un constructor recibe StripeClient, programaste contra una implementación; si recibe PasarelaPago, programaste contra una interfaz. Lo segundo te deja sustituir, testear y extender sin tocar el código que la usa.

Antes

class Carrito
{
    public function __construct(private MySqlProductRepository $repo) {}

    public function total(array $ids): float
    {
        $sum = 0;
        foreach ($ids as $id) $sum += $this->repo->find($id)->precio;
        return $sum;
    }
}

Después

interface ProductRepository
{
    public function find(int $id): Producto;
}

class Carrito
{
    public function __construct(private ProductRepository $repo) {}
    // total() igual que antes
}
¿Dónde aparece en SOLID / patrones?

DIP: esta es la formulación práctica del principio. Cuando ves "depende de abstracciones, no de concreciones", lo que se pide es exactamente esto: que el tipo declarado de las dependencias sea una interfaz, no una clase concreta. Casi todos los patrones GoF se apoyan en este hábito.

¿Detectaste el smell?

¿Por qué la versión del "después" facilita los tests?

Mapa POO ↔ SOLID ↔ Patrones

Esta tabla resume las conexiones. Volvé acá cuando necesites el atajo entre un concepto de POO y dónde reaparece más adelante.

Concepto de POO Principio SOLID Patrón representativo
EncapsulamientoSRP
AbstracciónDIPStrategy, Adapter
HerenciaLSPTemplate Method
PolimorfismoOCPStrategy, State
Interfaz vs clase abstractaISP
Composición sobre herenciaStrategy, Decorator, Composite
Programar contra interfacesDIP(todos los GoF)

Los guiones no significan "no aplica": significan "no es el ejemplo más útil de este eje". Casi cualquier concepto toca varios principios y varios patrones.