Patrones de diseño y arquitectura de software
"Un patrón describe un problema que ocurre repetidamente en nuestro ambiente y luego describe el núcleo de la solución para ese problema, de forma que podés usar esa solución un millón de veces sin hacerlo de la misma manera dos veces." — Christopher Alexander (original para arquitectura; adoptado por Gamma et al. para software).
Qué vas a aprender en este capítulo
Los patrones de diseño son soluciones reutilizables a problemas comunes de diseño de software. No son código que copiás — son plantillas de cómo estructurar clases y sus interacciones. Este capítulo cubre los principios SOLID (base teórica), los patrones GOF más usados (aplicación práctica) y los estilos arquitectónicos más comunes en la industria. Al terminar, podrás diseñar sistemas que sean fáciles de mantener, extender y probar.
5.1 Principios SOLID
💡 Intuición
SOLID es un acrónimo de cinco principios de diseño orientado a objetos que hacen el código más mantenible. No son reglas absolutas — son guías para tomar mejores decisiones cuando diseñás tus clases.
¿Para qué sirven? Para evitar el código espagueti: ese sistema donde tocás una cosa y se rompen cinco, donde agregar una función requiere modificar 10 archivos, donde nadie entiende qué hace qué.
📐 Fundamento
S — Single Responsibility Principle (SRP)
Una clase debe tener una sola razón para cambiar. Dicho de otro modo: una clase hace una sola cosa bien.
Violación: Una clase Pedido que además de representar el pedido también lo imprime, lo guarda en la BD y calcula el IVA.
Corrección: Pedido solo representa el pedido. PedidoRepository lo persiste. ImpresionService lo imprime. CalculadoraFiscal calcula el IVA.
O — Open/Closed Principle (OCP)
Las clases deben estar abiertas para extensión, cerradas para modificación. Se agrega funcionalidad sin modificar el código existente (usando herencia, interfaces o inyección de dependencias).
Violación: Un if/elif gigante para cada tipo de descuento. Agregar un descuento nuevo requiere modificar la función.
Corrección: Interfaz Descuento con método calcular(). Cada tipo de descuento implementa la interfaz. Para agregar uno nuevo, solo creás una nueva clase.
L — Liskov Substitution Principle (LSP)
Los objetos de una subclase deben poder reemplazar objetos de la superclase sin romper el programa. Si Pato hereda de Ave, y Ave tiene un método volar(), pero el PatoDeGoma no puede volar → violación del LSP.
I — Interface Segregation Principle (ISP)
Es mejor tener muchas interfaces específicas que una interfaz grande y genérica. Los clientes no deben depender de métodos que no usan.
Violación: Interfaz Trabajador con métodos trabajar(), comer(), dormir(). Los robots trabajadores deberían implementar solo trabajar() pero la interfaz los obliga a implementar los tres.
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 (interfaces).
Violación: PedidoService crea instancias de MySQLRepository directamente → imposible hacer pruebas sin una BD real.
Corrección: PedidoService depende de IPedidoRepository. MySQLRepository implementa esa interfaz. Se puede inyectar un MockRepository en las pruebas.
🛠️ En la práctica
Aplicando SRP en La Esquina:
Mala versión (todo en una clase):
class Pedido:
def calcular_total(self): ...
def guardar_en_bd(self): ... # viola SRP
def imprimir_ticket(self): ... # viola SRP
def enviar_email_confirmacion(self): ... # viola SRP
Buena versión (una razón por clase):
class Pedido:
def calcular_total(self): ... # Solo lógica de negocio
class PedidoRepository:
def guardar(self, pedido): ... # Solo persistencia
class TicketPrinter:
def imprimir(self, pedido): ... # Solo impresión
class NotificacionService:
def enviar_confirmacion(self, pedido): ... # Solo notificaciones
Aplicando DIP (Inyección de dependencias):
class PedidoService:
def __init__(self, repo: IPedidoRepository):
self.repo = repo # Depende de la interfaz, no de la implementación
# En producción:
service = PedidoService(MySQLRepository())
# En pruebas:
service = PedidoService(MockRepository())
5.2 Patrones de diseño GOF
💡 Intuición
En 1994, cuatro autores (Gang of Four — GOF: Gamma, Helm, Johnson, Vlissides) publicaron Design Patterns: Elements of Reusable Object-Oriented Software, catalogando 23 patrones de diseño. Es uno de los libros más influyentes de la informática.
Los patrones se dividen en tres categorías:
- Creacionales: Cómo se crean objetos.
- Estructurales: Cómo se organizan y combinan las clases.
- De comportamiento: Cómo se comunican y coordinan los objetos.
No necesitás memorizar los 23. Hay 5-7 que son omnipresentes en la industria.
📐 Fundamento
Patrones creacionales:
1. Factory Method
Problema: La clase A necesita crear objetos de tipo B, pero no sabe qué subtipo exacto de B crear.
Solución: Delegar la creación a un método fábrica que las subclases pueden sobreescribir.
class PlatilloFactory:
@staticmethod
def crear(tipo: str) -> Platillo:
if tipo == "pupusa":
return Pupusa()
elif tipo == "ensalada":
return Ensalada()
raise ValueError(f"Tipo desconocido: {tipo}")
2. Singleton
Problema: Solo debe existir una instancia de una clase (ej: conexión a la BD, configuración global).
Solución: La clase controla su propia instanciación.
class ConfiguracionApp:
_instancia = None
@classmethod
def get_instancia(cls):
if cls._instancia is None:
cls._instancia = cls()
return cls._instancia
Patrones estructurales:
3. Adapter
Convierte la interfaz de una clase en otra que el cliente espera. Es el "adaptador de enchufes" del software.
4. Decorator
Agrega funcionalidad a un objeto dinámicamente, sin modificar su clase. Alternativa flexible a la herencia.
class PedidoPrioridad:
def __init__(self, pedido: Pedido):
self._pedido = pedido
def calcular_total(self):
return self._pedido.calcular_total() + 5.00 # Cargo extra por prioridad
Patrones de comportamiento:
5. Observer (Publicador-Suscriptor)
Cuando un objeto cambia de estado, notifica automáticamente a todos los objetos que dependen de él.
class Pedido: # Publisher
def __init__(self):
self._observers = []
def suscribir(self, obs): self._observers.append(obs)
def cambiar_estado(self, nuevo_estado):
self.estado = nuevo_estado
for obs in self._observers:
obs.actualizar(self)
class PantallaCocinoa: # Subscriber
def actualizar(self, pedido):
print(f"Pedido #{pedido.id} ahora: {pedido.estado}")
6. Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables.
class CalculadoraDescuento:
def calcular(self, subtotal): ...
class DescuentoFestivo(CalculadoraDescuento):
def calcular(self, subtotal): return subtotal * 0.85 # 15% off
class SinDescuento(CalculadoraDescuento):
def calcular(self, subtotal): return subtotal
class Pedido:
def __init__(self, estrategia: CalculadoraDescuento):
self.estrategia = estrategia
def total_con_descuento(self, subtotal):
return self.estrategia.calcular(subtotal)
7. Template Method
Define el esqueleto de un algoritmo en una superclase, dejando que las subclases sobreescriban pasos específicos.
🛠️ En la práctica
Observer en La Esquina:
Cuando un pedido cambia a estado "Listo", múltiples sistemas quieren saberlo:
- La pantalla del mozo muestra una notificación.
- El sistema de estadísticas actualiza el tiempo promedio de preparación.
- (Futuro) El sistema de WhatsApp le avisa al cliente que ya está listo.
Con Observer, el Pedido no sabe quién está escuchando. Cada nuevo "observador" se suscribe sin modificar Pedido. Perfecto para extensibilidad (OCP).
Strategy para métodos de pago:
class MetodoPago:
def procesar(self, monto): ...
class PagoEfectivo(MetodoPago):
def procesar(self, monto):
return {"estado": "ok", "cambio": ...}
class PagoTarjeta(MetodoPago):
def procesar(self, monto):
# Llamar a pasarela de pago
return {"estado": "ok", "codigo_auth": "XY123"}
class Caja:
def cobrar(self, pedido, metodo: MetodoPago):
return metodo.procesar(pedido.calcular_total())
Agregar "pago con transferencia" solo requiere una nueva clase, no modificar Caja.
5.3 Arquitectura de software
💡 Intuición
La arquitectura de software es la estructura de alto nivel del sistema: cómo se organizan los grandes componentes (frontend, backend, base de datos, servicios externos) y cómo se comunican.
Es decisión de diseño a muy larga escala. Una mala arquitectura es costosísima de cambiar después. Una buena arquitectura soporta crecimiento sin tener que reescribir todo.
Las arquitecturas más comunes en sistemas empresariales:
- Monolito en capas
- MVC y variantes
- Microservicios
📐 Fundamento
Arquitectura en capas (N-Tier):
La más clásica y enseñada:
┌──────────────────────────────────┐
│ Capa de Presentación │ (UI: HTML, React, Android, etc.)
├──────────────────────────────────┤
│ Capa de Lógica de Negocio │ (Services, Use Cases)
├──────────────────────────────────┤
│ Capa de Acceso a Datos │ (Repositories, DAO)
├──────────────────────────────────┤
│ Base de Datos │ (MySQL, PostgreSQL, MongoDB)
└──────────────────────────────────┘
Regla: cada capa solo habla con la capa inmediatamente adyacente. La capa de presentación nunca habla directamente con la base de datos.
MVC — Model View Controller:
El patrón más usado en frameworks web (Django, Rails, Laravel, Spring MVC):
- Model: Los datos y la lógica de negocio (
Pedido,Platillo, repositorios). - View: La presentación al usuario (HTML, JSON).
- Controller: Recibe la solicitud del usuario, coordina el Model y devuelve la View.
Flujo: User → Controller → Model → Controller → View → User
Variantes:
- MVP (Model-View-Presenter): El Presenter hace todo el trabajo; la View es "tonta". Común en Android (pasado).
- MVVM (Model-View-ViewModel): El ViewModel expone datos como observables. La View se "auto-actualiza". Común en aplicaciones modernas (Vue.js, SwiftUI, Jetpack Compose).
Microservicios:
En lugar de un monolito único, el sistema se divide en servicios pequeños e independientes, cada uno con su propia base de datos y desplegable por separado.
Ventajas: escalabilidad independiente, tecnología heterogénea, despliegues independientes. Desventajas: complejidad operacional, comunicación de red (latencia, fallos), distributed tracing.
Para sistemas universitarios (ADS415): Empezad con monolito en capas. Microservicios son para organizaciones con cientos de desarrolladores. No es el problema de La Esquina.
🛠️ En la práctica
Arquitectura final de La Esquina:
Frontend (Web)
├── React o HTML/JS simple
└── Pantalla cocina (página dedicada con websockets)
Backend (Python/Django o Node.js/Express)
├── Controladores (rutas HTTP)
├── Servicios
│ ├── PedidoService
│ ├── MenuService
│ └── ReporteService
├── Repositorios
│ ├── PedidoRepository
│ ├── PlatilloRepository
│ └── EmpleadoRepository
└── Modelos (clases del dominio)
├── Pedido, ItemPedido
├── Platillo
└── Empleado, Mozo, Administrador
Base de Datos (MySQL o SQLite para empezar)
Decisiones arquitectónicas tomadas:
- Monolito en capas — el equipo es pequeño, el negocio es simple.
- Web sobre app nativa — funciona en cualquier dispositivo con browser; no requiere instalación.
- Websockets para cocina — la pantalla de cocina necesita actualizaciones en tiempo real sin que el cocinero aprete refresh.
- Un backend, múltiples clientes — el mismo backend sirve a mozos, cocina y administrador.
Diagramas de despliegue (texto simplificado):
Router WiFi La Esquina
├── Servidor (PC/Raspberry Pi en local)
│ └── [Docker: backend + BD]
├── Tablet mozo 1 ─── navegador → http://esquina.local/mozo
├── Tablet mozo 2 ─── navegador → http://esquina.local/mozo
└── TV/Monitor cocina ── navegador → http://esquina.local/cocina
5.4 Ejercicios
✏️ Ejercicio 5.1 — Principios SOLID
Identificá qué principio SOLID se viola en cada caso y proponé la corrección:
a. Una clase Usuario tiene métodos: login(), logout(), enviarEmail(), guardarEnBD(), generarReporte().
b. Una función calcularDescuento(tipo) tiene un if/elif con 8 tipos de descuento diferentes.
c. Un método volar() en la clase Ave, del que hereda Avestruz que lanza NotImplementedError.
Solución
a. Violación de SRP.
Usuario tiene demasiadas responsabilidades. Corrección:
Usuario: login(), logout() (autenticación)EmailService: enviarEmail()UsuarioRepository: guardarEnBD()ReporteService: generarReporte()
b. Violación de OCP.
Cada vez que se agrega un tipo de descuento, se modifica la función. Corrección:
class Descuento:
def calcular(self, monto): ...
class DescuentoEstudiante(Descuento):
def calcular(self, monto): return monto * 0.80
class DescuentoFestivo(Descuento):
def calcular(self, monto): return monto * 0.85
# Agregar nuevo tipo: solo nueva clase, sin tocar las existentes.
c. Violación de LSP.
Avestruz hereda de Ave pero no puede volar → comportamiento inesperado. Corrección: Separar interfaz. AveVoladora hereda de Ave y agrega volar(). Avestruz hereda de Ave directamente sin volar().
✏️ Ejercicio 5.2 — Identificar patrones
Identificá qué patrón GOF se aplica en cada situación:
a. Un sistema de logging que solo debe tener una instancia. b. El sistema envía notificaciones a la app del mozo, a la pantalla de cocina y al log del sistema cada vez que un pedido cambia de estado. c. El sistema puede calcular el total con diferentes estrategias: precio fijo, precio por peso, precio por tiempo.
Solución
a. Singleton — una sola instancia del logger en toda la aplicación.
b. Observer — el Pedido (publisher) notifica a múltiples suscriptores (app mozo, pantalla cocina, sistema de log) sin conocerlos directamente.
c. Strategy — diferentes algoritmos de cálculo intercambiables. La clase Pedido recibe la estrategia por inyección y la usa sin saber cuál es.
✏️ Ejercicio 5.3 — Diseño completo
Diseñá la arquitectura (en capas) de un sistema de ventas online para una tienda salvadoreña. El sistema debe:
- Mostrar productos con fotos y precios.
- Permitir al cliente agregar al carrito y hacer checkout.
- Procesar pago (por ahora, simulado).
- Enviar email de confirmación.
- Al administrador, mostrar ventas del día.
a. Listá las capas y qué componentes van en cada una. b. Identificá 3 patrones GOF que usarías y por qué. c. ¿Qué requerimientos no funcionales deberías considerar?
Solución
a. Capas:
Capa de Presentación: React (cliente) / HTML+CSS. Páginas: catálogo, carrito, checkout, panel admin.
Capa de Lógica de Negocio:
CarritoService: agregar/quitar productos, calcular totalCheckoutService: validar pedido, procesar pago, notificarReporteService: ventas del díaCatalogoService: listar y filtrar productos
Capa de Acceso a Datos:
ProductoRepository,PedidoRepository,ClienteRepository
Base de Datos: PostgreSQL (productos, pedidos, clientes).
Servicios externos: pasarela de pago, servicio de email (SendGrid/SES).
b. Patrones GOF:
-
Observer — cuando se crea un pedido, notificar: email al cliente, notificación al admin, actualización de inventario. El servicio de pedidos no sabe qué observadores hay.
-
Strategy — para el cálculo de envío (gratis si > $50, tarifa fija para San Miguel, tarifa por km para otros). Se intercambia la estrategia según la dirección.
-
Factory — para crear distintos tipos de notificación (email, SMS, push notification) sin que
CheckoutServiceconozca los detalles de implementación de cada canal.
c. RNF importantes:
- Seguridad: HTTPS obligatorio; nunca almacenar tarjetas; autenticación JWT.
- Rendimiento: catálogo carga < 2 segundos; cache de productos (Redis).
- Disponibilidad: 99.9% uptime; deploy en cloud (no en servidor local).
- Escalabilidad: soportar picos en Black Friday y navidades.
5.5 Cierre del libro
Con este capítulo completás el ciclo de análisis y diseño:
- Análisis: entender el problema (cap. 1 y 2).
- Modelo estático: diagrama de clases (cap. 3).
- Modelo dinámico: secuencia, actividad, estados (cap. 4).
- Diseño: principios SOLID, patrones, arquitectura (cap. 5).
El sistema de La Esquina ahora tiene:
- Requerimientos documentados en historias de usuario y casos de uso.
- Diagrama de clases con
Pedido,ItemPedido,Platillo,Mozo,Administrador. - Diagramas de secuencia para los casos de uso principales.
- Diagrama de estados del ciclo de vida del pedido.
- Arquitectura en capas con patrones Observer, Strategy y Factory.
Eso es lo que un equipo profesional produce antes de escribir la primera línea de código. Al llegar al código, las decisiones difíciles ya están tomadas.
5.6 Para profundizar
- Gamma et al., Design Patterns (el libro GOF original, 1994) — referencia obligatoria.
- Martin, Clean Architecture — Robert C. Martin sobre principios de arquitectura moderna.
- Freeman & Robson, Head First Design Patterns — versión amigable y visual del GOF.
- Ingeniería de Software — profundiza en metodologías, calidad y pruebas.
- Bases de Datos II — optimización, transacciones y arquitecturas de persistencia.
Definiciones nuevas: SOLID, SRP, OCP, LSP, ISP, DIP, patrón de diseño, Factory, Singleton, Adapter, Decorator, Observer, Strategy, Template Method, arquitectura en capas, MVC, MVP, MVVM, microservicios, monolito.