0% encontró este documento útil (0 votos)
2 vistas24 páginas

Guia Poo Solid Python

La Programación Orientada a Objetos se fundamenta en cuatro pilares: abstracción, encapsulamiento, herencia y polimorfismo, cada uno abordando problemas específicos en el diseño de software. Se presentan ejemplos prácticos en Python que ilustran cómo implementar estos conceptos, así como los principios SOLID que promueven un código más mantenible y extensible. Estos principios incluyen la responsabilidad única, apertura/cierre, sustitución de Liskov, segregación de interfaces e inversión de dependencias.
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
2 vistas24 páginas

Guia Poo Solid Python

La Programación Orientada a Objetos se fundamenta en cuatro pilares: abstracción, encapsulamiento, herencia y polimorfismo, cada uno abordando problemas específicos en el diseño de software. Se presentan ejemplos prácticos en Python que ilustran cómo implementar estos conceptos, así como los principios SOLID que promueven un código más mantenible y extensible. Estos principios incluyen la responsabilidad única, apertura/cierre, sustitución de Liskov, segregación de interfaces e inversión de dependencias.
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

SECCIÓN 1 — Pilares de la POO

La Programación Orientada a Objetos se basa en cuatro pilares fundamentales. Cada uno resuelve un
problema concreto de diseño de software.

1.1 Abstracción
Mostrar solo lo esencial, ocultar la complejidad interna. Se define QUÉ hace algo sin revelar CÓMO lo
hace.
Concepto clave para el parcial: una clase abstracta no puede instanciarse directamente. Si
intentas hacerlo, Python lanza TypeError.

Mecanismo en Python
• Importar ABC (Abstract Base Class) y abstractmethod del módulo abc
• Decorar métodos con @abstractmethod para que sean obligatorios en subclases
• Toda subclase DEBE implementar todos los métodos abstractos o también será abstracta

Ejemplo comentado — Formas geométricas


abstraccion_formas.py
from abc import ABC, abstractmethod
import math

# ABC convierte la clase en abstracta — NO se puede instanciar


class Forma(ABC):

@abstractmethod # obliga a las subclases a implementarlo


def area(self) -> float:
"""Retorna el área de la forma."""
... # el cuerpo no importa, jamás se ejecuta

@abstractmethod
def perimetro(self) -> float: ...

# Método CONCRETO: se hereda sin necesidad de redefinir


def describir(self) -> str:
return (f'{type(self).__name__}: '
f'area={[Link]():.2f}, perimetro={[Link]():.2f}')

class Circulo(Forma):
def __init__(self, radio: float):
[Link] = radio

def area(self) -> float: # implementación obligatoria


return [Link] * [Link] ** 2
def perimetro(self) -> float:
return 2 * [Link] * [Link]

class Rectangulo(Forma):
def __init__(self, ancho: float, alto: float):
[Link] = ancho
[Link] = alto

def area(self) -> float:


return [Link] * [Link]

def perimetro(self) -> float:


return 2 * ([Link] + [Link])

# Polimorfismo en acción: el código no sabe qué forma concreta es


formas: list[Forma] = [Circulo(5), Rectangulo(4, 6)]
for f in formas:
print([Link]())

Si intentas hacer Forma() directamente, obtienes: TypeError: Can't instantiate abstract class
Forma with abstract methods area, perimetro

Un método abstracto puede tener cuerpo — pero solo se llama con super(). Lo que lo hace
'abstracto' es que OBLIGA a la subclase a redefinirlo.

1.2 Encapsulamiento
Controlar qué se expone y qué se protege del exterior. Los datos internos no deben modificarse
libremente desde fuera del objeto.

Convenciones de privacidad en Python


Sintaxis Nivel Significado
nombre Público Accesible desde cualquier
lugar. Uso libre.
_nombre Privado por convención Señal de 'no uses esto desde
fuera'. Python no lo impide.
__nombre Name mangling activo Python lo renombra a
_Clase__nombre. Dificulta
acceso externo.
@property Acceso controlado Permite validar al leer o
escribir. Sin setter = solo
lectura.

Ejemplo comentado — Cuenta bancaria


encapsulamiento_banco.py
from datetime import datetime
class CuentaBancaria:
def __init__(self, titular: str, saldo_inicial: float = 0.0):
self._titular = titular # convención: privado
self.__saldo = 0.0 # name mangling: real privado
self.__historial: list = []
if saldo_inicial > 0:
self.__depositar_interno(saldo_inicial, 'Deposito inicial')

# @property = getter: permite leer __saldo sin modificarlo


@property
def saldo(self) -> float:
return self.__saldo
# NO hay setter -> [Link] = 999 lanzaria AttributeError

def depositar(self, monto: float) -> None:


if monto <= 0:
raise ValueError('El monto debe ser positivo')
self.__depositar_interno(monto, 'Deposito')

def retirar(self, monto: float) -> None:


if monto <= 0:
raise ValueError('El monto debe ser positivo')
if monto > self.__saldo: # validación antes de modificar
raise ValueError(f'Saldo insuficiente ({self.__saldo:.2f})')
self.__saldo -= monto

# Doble guion = método privado real


def __depositar_interno(self, monto: float, concepto: str) -> None:
self.__saldo += monto

cuenta = CuentaBancaria('Ana', 1000.0)


[Link](500)
[Link](200)
print([Link]) # 1300.0 — OK
# cuenta.__saldo = 999999 # NO funciona (name mangling)
# [Link] = 999999 # AttributeError: no hay setter

@property con setter — Termostato


encapsulamiento_termostato.py
class Termostato:
TEMP_MIN = -273.15
TEMP_MAX = 1000.0

def __init__(self, temp_inicial: float = 20.0):


self.__temperatura = None
[Link] = temp_inicial # usa el setter desde __init__

@property
def temperatura(self) -> float:
return self.__temperatura

@[Link] # mismo nombre + .setter


def temperatura(self, valor: float) -> None:
if not (self.TEMP_MIN <= valor <= self.TEMP_MAX):
raise ValueError(f'Temperatura fuera de rango: {valor}°C')
self.__temperatura = valor

t = Termostato(22.0)
[Link] = 37.5 # usa el setter, valida
[Link] = -300 # ValueError!
Pregunta frecuente de parcial: ¿cuál es la diferencia entre _x y __x? La respuesta: _x es solo
convención (Python no la hace cumplir), __x activa name mangling (Python la renombra
internamente).

1.3 Herencia
Una clase adquiere atributos y métodos de otra. La subclase puede usar lo heredado, sobreescribirlo
con super() y agregar comportamiento nuevo.

Ejemplo comentado — Empleados


herencia_empleados.py
class Empleado:
_contador_ids: int = 0 # variable de clase compartida

def __init__(self, nombre: str, salario_base: float):


Empleado._contador_ids += 1
[Link] = Empleado._contador_ids
[Link] = nombre
self._salario_base = salario_base

def calcular_salario(self) -> float:


return self._salario_base

class EmpleadoComision(Empleado): # hereda de Empleado


def __init__(self, nombre, salario_base, ventas, tasa=0.10):
super().__init__(nombre, salario_base) # llama al padre
[Link] = ventas
self.tasa_comision = tasa

def calcular_salario(self) -> float:


# sobreescribe Y extiende el método del padre
return super().calcular_salario() + ([Link] * self.tasa_comision)

class Gerente(Empleado):
def __init__(self, nombre, salario_base, bono):
super().__init__(nombre, salario_base)
[Link] = bono
[Link]: list[Empleado] = []

def calcular_salario(self) -> float:


return super().calcular_salario() + [Link]

def info(self) -> str:


base = super().info() # reutiliza lógica del padre
return f'{base} | Equipo: {len([Link])} personas'

Herencia múltiple y MRO


herencia_vehiculos.py
class Vehiculo:
def __init__(self, marca, modelo, anio):
[Link] = marca

class Electrico:
def __init__(self, capacidad_bateria, **kwargs):
super().__init__(**kwargs) # MRO: pasa args al siguiente
self.capacidad_bateria = capacidad_bateria

# Herencia múltiple: orden importa para el MRO


class AutoElectrico(Electrico, Vehiculo):
def __init__(self, marca, modelo, anio, capacidad_bateria):
super().__init__(marca=marca, modelo=modelo, anio=anio,
capacidad_bateria=capacidad_bateria)

# Ver el orden de resolución de métodos:


print(AutoElectrico.__mro__)
# (<AutoElectrico> -> <Electrico> -> <Vehiculo> -> <object>)

MRO (Method Resolution Order): Python usa el algoritmo C3 para determinar el orden en que
busca métodos en herencia múltiple. __mro__ te muestra ese orden.

Regla para super() con herencia múltiple: siempre usa **kwargs para pasar argumentos
desconocidos hacia arriba en la cadena MRO.

1.4 Polimorfismo
El mismo mensaje, comportamientos distintos según quién lo recibe. Objetos de distintas clases
responden al mismo método de forma diferente.

Tipo Mecanismo Ejemplo


Por herencia Sobreescribir métodos class Perro(Animal): def
hacer_sonido(): return 'Guau'
Duck typing Mismo método, sin relación de Cualquier objeto
herencia con .renderizar() funciona
Dunder methods Operadores sobrecargados __add__, __len__, __str__,
__eq__, ...

Duck typing — sin herencia formal


duck_typing.py
# Estas clases NO tienen relación de herencia entre sí
class Titulo:
def renderizar(self) -> str:
return f'# {[Link]}'

class MensajeSimple:
def renderizar(self) -> str:
return f'>> {[Link]}'

# La función acepta CUALQUIER cosa que tenga .renderizar()


# Python no exige que hereden de la misma clase
def imprimir_todo(items) -> None:
for item in items: # duck typing puro
print([Link]())

imprimir_todo([Titulo(), MensajeSimple()]) # funciona!

Dunder methods — operadores sobrecargados


dunder_methods.py
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, otro): # v1 + v2


return Vector2D(self.x + otro.x, self.y + otro.y)

def __mul__(self, escalar): # v * 2


return Vector2D(self.x * escalar, self.y * escalar)

def __abs__(self): # abs(v)


return (self.x**2 + self.y**2) ** 0.5

def __len__(self): # len(v)


return 2

def __eq__(self, otro): # v1 == v2


return self.x == otro.x and self.y == otro.y

def __repr__(self): # repr(v) y en la consola


return f'Vector2D({self.x}, {self.y})'

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1 + v2) # Vector2D(4, 6)
print(abs(v1)) # 5.0
print(len(v1)) # 2

Dunder methods más importantes: __init__, __str__, __repr__, __len__, __add__, __sub__,
__mul__, __eq__, __lt__, __getitem__, __iter__, __contains__
SECCIÓN 2 — Principios SOLID
SOLID es un acrónimo de cinco principios de diseño orientado a objetos que hacen el código más
mantenible, extensible y testeable.

Letra Nombre Regla


S Single Responsibility Una clase = una razón para
cambiar
O Open/Closed Abierto para extensión,
cerrado para modificación
L Liskov Substitution Una subclase debe poder
reemplazar al padre sin
romper nada
I Interface Segregation Interfaces pequeñas, no una
interfaz gigante
D Dependency Inversion Depender de abstracciones,
no de implementaciones
concretas

S — Single Responsibility Principle (SRP)


Una clase debe tener una sola razón para cambiar. Si puedes describir la clase usando 'y' en la mitad,
la estás dividiendo.

Violación ✗ Correcto ✓
class Pedido: class Pedido: # solo
# Hace TRES cosas: datos
def def
calcular_total(self): ... calcular_total(self): ...
def guardar_en_db(self): ...
def class RepositorioPedido: # solo
enviar_confirmacion(self): ... DB
def guardar(self, p): ...

class EmailPedido: # solo


email
def confirmar(self, p): ...

Ejemplo completo — Factura


srp_factura.py
from dataclasses import dataclass, field

@dataclass
class LineaFactura:
descripcion: str
cantidad: int
precio_unitario: float

@property
def subtotal(self) -> float:
return [Link] * self.precio_unitario

# CLASE 1: solo representa la factura y su lógica de negocio


@dataclass
class Factura:
numero: str
cliente: str
lineas: list[LineaFactura] = field(default_factory=list)

def total(self) -> float:


return sum([Link] for l in [Link])

# CLASE 2: solo se encarga de formatear/imprimir


class ImpresoraFactura:
def imprimir(self, factura: Factura) -> str:
lineas = [f'FACTURA #{[Link]}',
f'Cliente: {[Link]}', '-' * 30]
for l in [Link]:
[Link](f' {[Link]:<20} ${[Link]:>8.2f}')
[Link](f' TOTAL ${[Link]():>8.2f}')
return ' '.join(lineas)

# CLASE 3: solo se encarga de persistencia


class RepositorioFactura:
def __init__(self):
self._almacen: dict[str, Factura] = {}

def guardar(self, factura: Factura) -> None:


self._almacen[[Link]] = factura

def buscar(self, numero: str) -> Factura | None:


return self._almacen.get(numero)

Truco mental SRP: si para describir la clase necesitas decir 'y' ('guarda datos Y envía emails'),
entonces tiene más de una responsabilidad.

O — Open/Closed Principle (OCP)


Abierto para extensión, cerrado para modificación. Agregar nueva funcionalidad NO debe requerir
editar código existente.

Violación ✗ Correcto ✓
def calcular_descuento(tipo, class Descuento(ABC):
precio): @abstractmethod
if tipo == 'vip': def aplicar(self,
return precio * 0.8 precio): ...
elif tipo == 'estudiante':
return precio * 0.9 class DescuentoVIP(Descuento):
# Nuevo tipo = EDITAR esta def aplicar(self, p): return
función p * 0.8
# Nuevo tipo = nueva clase, sin
editar nada
Ejemplo completo — Exportadores extensibles
ocp_exportadores.py
from abc import ABC, abstractmethod
import json

# Contrato base: nunca cambia


class Exportador(ABC):
@abstractmethod
def exportar(self, datos: list[dict]) -> str: ...

class ExportadorCSV(Exportador):
def exportar(self, datos: list[dict]) -> str:
if not datos: return ''
cabecera = ','.join(datos[0].keys())
filas = [','.join(str(v) for v in [Link]()) for d in datos]
return ' '.join([cabecera] + filas)

class ExportadorJSON(Exportador):
def exportar(self, datos: list[dict]) -> str:
return [Link](datos, ensure_ascii=False, indent=2)

class ExportadorMarkdown(Exportador):
def exportar(self, datos: list[dict]) -> str:
cols = list(datos[0].keys())
sep = ' | '
divisor = [Link](['---'] * len(cols))
filas = [[Link](str(d[c]) for c in cols) for d in datos]
return ' '.join([[Link](cols), divisor] + filas)

# ServicioReportes NUNCA cambia aunque agreguemos exportadores


class ServicioReportes:
def __init__(self, exportador: Exportador):
[Link] = exportador

def generar(self, datos: list[dict]) -> str:


return [Link](datos)

# Agregar ExportadorXML no requiere tocar ServicioReportes

OCP y los if/elif: cuando una función tiene una cadena larga de if/elif basada en 'tipo', es
candidata a ser refactorizada con OCP. Cada rama se convierte en una clase.

L — Liskov Substitution Principle (LSP)


Una subclase debe poder reemplazar a su clase padre sin romper el programa. Si necesitas preguntar
'¿es un Perro o un Gato?' para comportarte diferente, algo está mal.

El problema clásico: Cuadrado hereda de Rectángulo

Violación ✗ Correcto ✓
class Rectangulo: class Forma(ABC): # padre
def __init__(self, ancho, común
alto): @abstractmethod
[Link] = ancho def area(self): ...
[Link] = alto
class Rectangulo(Forma): #
class Cuadrado(Rectangulo): hermano
@[Link] def area(self):
def ancho(self, v): return [Link] *
# cambiar ancho cambia [Link]
alto también
self._ancho = self._alto class Cuadrado(Forma): #
= v hermano
# duplicar_ancho() da resultado def area(self):
inesperado! return [Link] ** 2

Forma correcta — jerarquía con abstracción compartida


lsp_formas.py
from abc import ABC, abstractmethod

class Forma(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimetro(self) -> float: ...

class Rectangulo(Forma):
def __init__(self, ancho: float, alto: float):
[Link] = ancho
[Link] = alto
def area(self) -> float: return [Link] * [Link]
def perimetro(self) -> float: return 2 * ([Link] + [Link])

class Cuadrado(Forma): # HERMANO de Rectangulo, no hijo


def __init__(self, lado: float):
[Link] = lado
def area(self) -> float: return [Link] ** 2
def perimetro(self) -> float: return 4 * [Link]

# Esta función acepta CUALQUIER Forma sin importar el tipo concreto


def describir_forma(f: Forma) -> None:
print(f'{type(f).__name__}: area={[Link]():.2f}')

for forma in [Rectangulo(4, 5), Cuadrado(4)]:


describir_forma(forma) # funciona para ambos

Señales de violación LSP: una subclase lanza NotImplementedError en un método heredado;


una subclase tiene precondiciones más estrictas; el código cliente usa isinstance() para
comportarse diferente con cada subclase.

I — Interface Segregation Principle (ISP)


Ninguna clase debe depender de métodos que no usa. Prefiere muchas interfaces pequeñas sobre
una interfaz grande.
Violación ✗ Correcto ✓
class Trabajador(ABC): class Laborable(ABC):
@abstractmethod @abstractmethod
def trabajar(self): ... def trabajar(self): ...
@abstractmethod
def comer(self): ... class Biologico(ABC):
@abstractmethod
class Robot(Trabajador): def comer(self): ...
def trabajar(self): ...
def comer(self): pass # class Robot(Laborable): ...
forzado! # solo trabaja
class Humano(Laborable,
Biologico): ...

Ejemplo completo — Impresoras


isp_impresoras.py
from abc import ABC, abstractmethod

# Interfaces pequeñas y enfocadas


class Imprimible(ABC):
@abstractmethod
def imprimir(self, documento: str) -> None: ...

class Escaneable(ABC):
@abstractmethod
def escanear(self) -> str: ...

class Faxeable(ABC):
@abstractmethod
def enviar_fax(self, numero: str, doc: str) -> None: ...

# Implementa solo las interfaces que puede cumplir


class ImpresoraBasica(Imprimible):
def imprimir(self, documento: str) -> None:
print(f'[Basica] Imprimiendo: {documento[:40]}...')

class ImpresoraMultifuncion(Imprimible, Escaneable, Faxeable):


def imprimir(self, documento: str) -> None:
print(f'[Multi] Imprimiendo en alta calidad')
def escanear(self) -> str:
return '[Multi] Escaneado a 600 DPI'
def enviar_fax(self, numero: str, doc: str) -> None:
print(f'[Multi] Fax enviado a {numero}')

# Funciones que dependen solo de la interfaz que necesitan


def imprimir_reporte(imp: Imprimible, reporte: str) -> None:
[Link](reporte) # no importa si tiene fax o no

def digitalizar(scan: Escaneable) -> str:


return [Link]()

Señal de violación ISP: una clase implementa una interfaz y tiene que dejar métodos vacíos
(pass) o lanzar NotImplementedError. Es señal de que la interfaz es demasiado grande.
D — Dependency Inversion Principle (DIP)
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de
abstracciones. Esto se logra con inyección de dependencias.

Violación ✗ Correcto ✓
class ServicioUsuario: class ServicioUsuario:
def __init__(self): def __init__(self,
# Acoplado a MySQL db: BaseDatos,
específicamente email:
[Link] = ServicioEmail):
MySQLConexion() [Link] = db
[Link] = GmailSMTP() [Link] = email
# Imposible testear sin # Facil de testear con mocks
MySQL real

Ejemplo completo con mocks para testing


dip_notificaciones.py
from abc import ABC, abstractmethod

# Abstracciones (contratos)
class RepositorioUsuarios(ABC):
@abstractmethod
def buscar_por_id(self, uid: int) -> dict | None: ...
@abstractmethod
def guardar(self, usuario: dict) -> None: ...

class ServicioEmail(ABC):
@abstractmethod
def enviar(self, destino: str, asunto: str, cuerpo: str) -> bool: ...

# Implementaciones concretas (bajo nivel)


class RepositorioMemoria(RepositorioUsuarios):
def __init__(self):
self._datos: dict[int, dict] = {}
def buscar_por_id(self, uid): return self._datos.get(uid)
def guardar(self, usuario): self._datos[usuario['id']] = usuario

class EmailConsola(ServicioEmail):
def enviar(self, destino, asunto, cuerpo) -> bool:
print(f'Email a {destino}: {asunto}')
return True

# Módulo de alto nivel: depende SOLO de abstracciones


class ServicioRegistro:
def __init__(self,
repo: RepositorioUsuarios, # abstracción
email: ServicioEmail): # abstracción
self._repo = repo
self._email = email
self._siguiente_id = 1

def registrar(self, nombre: str, email_addr: str) -> dict:


usuario = {'id': self._siguiente_id, 'nombre': nombre,
'email': email_addr}
self._siguiente_id += 1
self._repo.guardar(usuario)
self._email.enviar(email_addr, 'Bienvenido',
f'Hola {nombre}, tu cuenta fue creada.')
return usuario

# Para testing: se reemplazan dependencias sin tocar ServicioRegistro


class EmailMock(ServicioEmail):
def __init__(self): [Link] = []
def enviar(self, d, a, c) -> bool:
[Link]({'destino': d, 'asunto': a})
return True

mock_email = EmailMock()
servicio_test = ServicioRegistro(RepositorioMemoria(), mock_email)
servicio_test.registrar('Test User', 'test@[Link]')
print(mock_email.enviados) # [{'destino': 'test@[Link]', ...}]

La inyección de dependencias no requiere ningún framework. Es simplemente pasar las


dependencias como argumentos al constructor en lugar de crearlas internamente.

Ventaja clave para testing: al inyectar dependencias, puedes reemplazarlas con mocks
(objetos falsos) que simulan el comportamiento sin usar servicios reales (base de datos, email,
etc.).
SECCIÓN 3 — Patrones de Diseño en Python
Los patrones de diseño son soluciones reutilizables a problemas comunes de diseño de software. Se
dividen en tres categorías principales.

Categoría Propósito Ejemplos


Creacionales Cómo se crean los objetos Singleton, Factory, Builder,
Prototype
Estructurales Cómo se componen los Decorator, Adapter, Facade,
objetos Proxy
De comportamiento Cómo se comunican los Observer, Strategy,
objetos Command, Iterator

3.1 Singleton — Creacional


Garantiza que una clase tenga una sola instancia y proporciona un punto de acceso global a ella. Útil
para configuraciones, conexiones de base de datos, loggers.

Implementación con __new__


patron_singleton.py
class Configuracion:
_instancia = None # almacena la única instancia

def __new__(cls):
# Si no existe instancia, crearla; si existe, retornarla
if cls._instancia is None:
cls._instancia = super().__new__(cls)
cls._instancia._inicializado = False
return cls._instancia

def __init__(self):
if self._inicializado: # evitar re-inicializar
return
[Link] = False
self.db_url = 'postgresql://localhost/mydb'
self._inicializado = True

# Comprobación: ambas variables apuntan al mismo objeto


c1 = Configuracion()
c2 = Configuracion()
print(c1 is c2) # True — misma instancia
[Link] = True
print([Link]) # True — el cambio se refleja
print(id(c1) == id(c2)) # True — misma dirección en memoria

Alternativa más pythónica: usar un módulo Python. Los módulos son singletons por naturaleza
— solo se importan una vez.

3.2 Factory Method — Creacional


Define una interfaz para crear objetos, pero deja que las subclases decidan qué clase instanciar.
Desacopla la creación del objeto de su uso.

Implementación
patron_factory.py
from abc import ABC, abstractmethod

# Producto abstracto
class Notificacion(ABC):
@abstractmethod
def enviar(self, mensaje: str) -> None: ...

# Productos concretos
class NotificacionEmail(Notificacion):
def __init__(self, email: str):
[Link] = email
def enviar(self, mensaje: str) -> None:
print(f'Email a {[Link]}: {mensaje}')

class NotificacionSMS(Notificacion):
def __init__(self, telefono: str):
[Link] = telefono
def enviar(self, mensaje: str) -> None:
print(f'SMS a {[Link]}: {mensaje}')

class NotificacionPush(Notificacion):
def __init__(self, device_id: str):
self.device_id = device_id
def enviar(self, mensaje: str) -> None:
print(f'Push a {self.device_id}: {mensaje}')

# Factory: encapsula la lógica de creación


class FabricaNotificaciones:
_tipos = {
'email': NotificacionEmail,
'sms': NotificacionSMS,
'push': NotificacionPush,
}

@classmethod
def crear(cls, tipo: str, destino: str) -> Notificacion:
if tipo not in cls._tipos:
raise ValueError(f'Tipo desconocido: {tipo}')
return cls._tipos[tipo](destino) # instancia la clase correcta

# El código cliente no sabe qué clase concreta usa


n1 = [Link]('email', 'user@[Link]')
n2 = [Link]('sms', '+573001234567')
[Link]('Tu pedido fue confirmado')
[Link]('Tu pedido fue confirmado')

El Factory Method resuelve el mismo problema que el OCP: si necesitas agregar un nuevo tipo
de notificación, solo agregas la clase y la registras en el diccionario _tipos, sin modificar el código
existente.

3.3 Decorator — Estructural


Agrega comportamiento a un objeto dinámicamente, sin modificar su clase. En Python existe la sintaxis
@decorador que facilita su uso.

Decorador de funciones (sintaxis Python)


patron_decorador_funciones.py
import time
import functools

# Decorador que mide el tiempo de ejecución


def medir_tiempo(func):
@[Link](func) # preserva nombre y docstring
def wrapper(*args, **kwargs):
inicio = [Link]()
resultado = func(*args, **kwargs) # llama la función original
fin = [Link]()
print(f'{func.__name__} tardó {fin - inicio:.4f}s')
return resultado
return wrapper

# Decorador de caché simple


def cache(func):
_cache = {}
@[Link](func)
def wrapper(*args):
if args not in _cache:
_cache[args] = func(*args)
return _cache[args]
return wrapper

@medir_tiempo # equivale a: calcular_fib = medir_tiempo(calcular_fib)


@cache # se aplican de abajo hacia arriba: cache primero
def calcular_fib(n: int) -> int:
if n <= 1: return n
return calcular_fib(n - 1) + calcular_fib(n - 2)

print(calcular_fib(35)) # rápido gracias al cache

Decorador de clases (patrón GoF)


patron_decorador_clases.py
from abc import ABC, abstractmethod

class Cafe(ABC):
@abstractmethod
def costo(self) -> float: ...
@abstractmethod
def descripcion(self) -> str: ...

class CafeSimple(Cafe):
def costo(self) -> float: return 1.0
def descripcion(self) -> str: return 'Cafe'

# Decorador base: envuelve un Cafe y delega


class DecoradorCafe(Cafe):
def __init__(self, cafe: Cafe):
self._cafe = cafe # referencia al objeto decorado

def costo(self) -> float:


return self._cafe.costo() # delega al envuelto
def descripcion(self) -> str:
return self._cafe.descripcion()

# Decoradores concretos: agregan comportamiento


class ConLeche(DecoradorCafe):
def costo(self) -> float:
return self._cafe.costo() + 0.5 # base + extra
def descripcion(self) -> str:
return self._cafe.descripcion() + ' + Leche'

class ConVainilla(DecoradorCafe):
def costo(self) -> float: return self._cafe.costo() + 0.75
def descripcion(self) -> str:
return self._cafe.descripcion() + ' + Vainilla'

# Encadenamiento dinámico de decoradores


pedido = ConVainilla(ConLeche(CafeSimple()))
print([Link]()) # Cafe + Leche + Vainilla
print([Link]()) # 2.25

3.4 Observer — Comportamiento


Define una dependencia uno-a-muchos entre objetos: cuando uno cambia de estado, todos sus
dependientes son notificados automáticamente. Implementa el patrón publicador/suscriptor.

Implementación
patron_observer.py
from abc import ABC, abstractmethod

# Interfaz del observador


class Observador(ABC):
@abstractmethod
def actualizar(self, evento: str, datos: dict) -> None: ...

# Sujeto: el objeto que notifica


class TiendaOnline:
def __init__(self):
self._observadores: list[Observador] = []
self._stock: dict[str, int] = {}

def suscribir(self, obs: Observador) -> None:


self._observadores.append(obs)

def desuscribir(self, obs: Observador) -> None:


self._observadores.remove(obs)

def _notificar(self, evento: str, datos: dict) -> None:


for obs in self._observadores:
[Link](evento, datos) # notifica a todos

def actualizar_stock(self, producto: str, cantidad: int) -> None:


self._stock[producto] = cantidad
if cantidad == 0:
self._notificar('sin_stock', {'producto': producto})
elif cantidad < 5:
self._notificar('stock_bajo', {'producto': producto, 'cantidad':
cantidad})
# Observadores concretos: reaccionan al evento
class NotificadorEmail(Observador):
def actualizar(self, evento: str, datos: dict) -> None:
print(f'[Email] Evento {evento}: {datos}')

class SistemaReposicion(Observador):
def actualizar(self, evento: str, datos: dict) -> None:
if evento == 'stock_bajo':
print(f'[Reposicion] Solicitar mas {datos["producto"]}')

tienda = TiendaOnline()
[Link](NotificadorEmail())
[Link](SistemaReposicion())
tienda.actualizar_stock('Laptop', 3) # notifica a ambos

3.5 Strategy — Comportamiento


Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Permite cambiar el
algoritmo de un objeto en tiempo de ejecución.

Implementación — Ordenamiento
patron_strategy.py
from abc import ABC, abstractmethod

# Interfaz de la estrategia
class EstrategiaOrdenamiento(ABC):
@abstractmethod
def ordenar(self, datos: list) -> list: ...

# Estrategias concretas: distintos algoritmos


class OrdenarAscendente(EstrategiaOrdenamiento):
def ordenar(self, datos: list) -> list:
return sorted(datos)

class OrdenarDescendente(EstrategiaOrdenamiento):
def ordenar(self, datos: list) -> list:
return sorted(datos, reverse=True)

class OrdenarPorLongitud(EstrategiaOrdenamiento):
def ordenar(self, datos: list) -> list:
return sorted(datos, key=len)

# Contexto: usa la estrategia sin conocer sus detalles


class Procesador:
def __init__(self, estrategia: EstrategiaOrdenamiento):
self._estrategia = estrategia

def cambiar_estrategia(self, nueva: EstrategiaOrdenamiento) -> None:


self._estrategia = nueva # cambio en tiempo de ejecución

def procesar(self, datos: list) -> list:


return self._estrategia.ordenar(datos)
p = Procesador(OrdenarAscendente())
print([Link]([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]

p.cambiar_estrategia(OrdenarDescendente())
print([Link]([3, 1, 4, 1, 5])) # [5, 4, 3, 1, 1]

# En Python también se puede pasar una función directamente


p.cambiar_estrategia(OrdenarPorLongitud())
print([Link](['banana', 'kiwi', 'fresa'])) # por longitud

Strategy vs if/elif: si tienes un if/elif largo que elige entre distintos algoritmos, es candidato a
refactorizarse con Strategy. Cada rama se convierte en una clase concreta.

3.6 Builder — Creacional


Separa la construcción de un objeto complejo de su representación, permitiendo que el mismo proceso
de construcción genere distintas representaciones. Útil para objetos con muchos parámetros
opcionales.

Implementación
patron_builder.py
from dataclasses import dataclass, field

@dataclass
class Pizza:
tamanio: str
masa: str
salsa: str
ingredientes: list[str] = field(default_factory=list)
extra_queso: bool = False

def __str__(self):
ings = ', '.join([Link])
return (f'Pizza {[Link]} | Masa: {[Link]} | '
f'Salsa: {[Link]} | Toppings: {ings}')

# Builder: construye la Pizza paso a paso


class PizzaBuilder:
def __init__(self):
self._tamanio = 'mediana'
self._masa = 'clasica'
self._salsa = 'tomate'
self._ingredientes = []
self._extra_queso = False

def tamanio(self, t: str) -> 'PizzaBuilder':


self._tamanio = t
return self # retorna self para encadenamiento fluido

def masa(self, m: str) -> 'PizzaBuilder':


self._masa = m
return self

def salsa(self, s: str) -> 'PizzaBuilder':


self._salsa = s
return self
def agregar_ingrediente(self, i: str) -> 'PizzaBuilder':
self._ingredientes.append(i)
return self

def extra_queso(self) -> 'PizzaBuilder':


self._extra_queso = True
return self

def construir(self) -> Pizza: # crea el objeto final


return Pizza(self._tamanio, self._masa, self._salsa,
self._ingredientes[:], self._extra_queso)

# API fluida: cada método retorna self


pizza = (PizzaBuilder()
.tamanio('grande')
.masa('delgada')
.salsa('barbacoa')
.agregar_ingrediente('pollo')
.agregar_ingrediente('cebolla')
.extra_queso()
.construir())
print(pizza)

3.7 Adapter — Estructural


Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Permite que clases
incompatibles trabajen juntas.

Implementación
patron_adapter.py
# Clase existente con interfaz incompatible
class SistemaViejoTemperatura:
def obtener_temp_fahrenheit(self) -> float:
return 98.6 # devuelve en Fahrenheit

# Interfaz que espera el sistema nuevo


class SensorTemperatura:
def obtener_celsius(self) -> float: ...

# Adapter: traduce entre las dos interfaces


class AdaptadorTemperatura(SensorTemperatura):
def __init__(self, sistema_viejo: SistemaViejoTemperatura):
self._viejo = sistema_viejo

def obtener_celsius(self) -> float:


fahrenheit = self._viejo.obtener_temp_fahrenheit()
return (fahrenheit - 32) * 5 / 9 # conversión

# El sistema nuevo solo conoce SensorTemperatura


def mostrar_temperatura(sensor: SensorTemperatura) -> None:
print(f'Temperatura: {sensor.obtener_celsius():.1f}°C')
viejo = SistemaViejoTemperatura()
adaptado = AdaptadorTemperatura(viejo) # envuelve el viejo
mostrar_temperatura(adaptado) # 37.0°C

El Adapter es útil cuando trabajas con librerías de terceros cuya interfaz no coincide con la que
espera tu sistema. Envuelves la librería en un adapter en lugar de modificarla.
SECCIÓN 4 — Resumen y Puntos Clave para el Parcial

4.1 Preguntas frecuentes de parcial


Sobre los pilares de POO
• ¿Por qué no se puede instanciar una clase abstracta? → Porque tiene métodos sin
implementación. Python lanza TypeError al intentarlo.
• ¿Diferencia entre _x y __x? → _x es solo convención; __x activa name mangling (Python lo
renombra a _Clase__x).
• ¿Qué hace super()? → Llama al método del padre inmediato según el MRO. Esencial para no
duplicar código en herencia.
• ¿Qué es el MRO? → Method Resolution Order. El orden en que Python busca métodos en
herencia múltiple. Ver con Clase.__mro__
• ¿Diferencia entre polimorfismo por herencia y duck typing? → Herencia requiere clase padre
común; duck typing no requiere relación, solo el mismo método.

Sobre SOLID
• SRP: ¿cuándo violo SRP? → Cuando una clase tiene más de una razón para cambiar (guarda
datos Y envía emails).
• OCP: ¿cómo agrego funcionalidad sin modificar? → Creando nuevas clases que implementan
la abstracción existente.
• LSP: ¿cuándo viola LSP una subclase? → Cuando lanza NotImplementedError, cuando usa
isinstance() para comportarse diferente, o cuando tiene precondiciones más estrictas.
• ISP: ¿señal de violación? → Una clase implementa una interfaz y deja métodos vacíos (pass) o
lanza NotImplementedError.
• DIP: ¿diferencia entre inyección de dependencias y dependencia directa? → En DI las
dependencias vienen del exterior; en directa la clase las crea con MySQLConexion() etc.

Sobre Patrones de Diseño


• Singleton: garantiza una única instancia. Se implementa con __new__. Los módulos Python
son singletons naturales.
• Factory: desacopla la creación del uso. El cliente no sabe qué clase concreta recibe.
• Decorator: agrega comportamiento sin modificar la clase. @[Link] preserva
metadatos de la función original.
• Observer: notificación automática de cambios. Implementa el patrón publicador/suscriptor.
• Strategy: familia de algoritmos intercambiables. Similar a OCP — cada algoritmo es una clase.
• Builder: construcción paso a paso de objetos complejos. API fluida gracias a return self.
• Adapter: compatibilidad entre interfaces incompatibles. Útil con librerías de terceros.

4.2 Tabla comparativa — Cuándo usar cada patrón SOLID


Principio Señal de violación Solución
SRP Clase con múltiples razones Separar en clases con
de cambio responsabilidades únicas
OCP Función con if/elif para cada Jerarquía de clases, cada tipo
tipo = clase
LSP Subclase sobreescribe con Rediseñar jerarquía, usar
raise NotImplementedError abstracción común
ISP Clase con métodos vacíos Dividir en interfaces pequeñas
(pass)
DIP Clase crea sus dependencias Inyectar dependencias como
con ConcreteClass() parámetros

4.3 Tabla comparativa — Patrones de diseño


Patrón Tipo Problema que Casos de uso típicos
resuelve
Singleton Creacional Una sola instancia Config, Logger, Pool
de conexiones
Factory Creacional Crear sin saber qué Notificaciones,
clase concreta parsers, exportadores
Builder Creacional Objeto con muchos Constructores de
parámetros queries, pizzas,
opcionales reportes
Decorator Estructural Agregar Logging, caché,
comportamiento autenticación
dinámicamente
Adapter Estructural Incompatibilidad de Integración con APIs
interfaces legacy o librerías
Observer Comportamiento Notificación Eventos, UI, sistemas
automática de de alertas
cambios
Strategy Comportamiento Algoritmos Ordenamiento,
intercambiables descuentos,
validaciones

4.4 Relaciones entre conceptos


Entender cómo se conectan estos conceptos es clave para el parcial:
• Abstracción + Polimorfismo → base de OCP y del patrón Strategy
• Encapsulamiento → base del patrón Singleton y de LSP (el contrato del objeto no se rompe)
• Herencia + Polimorfismo → permiten LSP y el patrón Decorator
• Abstracción + DIP → permiten el patrón Factory y el testing con mocks
• ISP + SRP → interfaces y clases pequeñas, de responsabilidad única
Error común: confundir el patrón Decorator de Python (@decorador) con el patrón GoF
Decorator. Son conceptos relacionados pero distintos. En el parcial especifica cuál es cuál.

Recuerda: los principios SOLID no son reglas absolutas, son guías. A veces es válido ignorar
uno en favor de simplicidad, especialmente en proyectos pequeños.

También podría gustarte