
Fundamentos de Programación: Patrones de Diseño y Arquitectura de Software
Aprende patrones de diseño y principios de arquitectura de software con ejemplos prácticos en Python.
¡Domina los patrones de diseño y arquitectura de software! En este tutorial avanzado te guiaré paso a paso para que aprendas los principios fundamentales de diseño de software, patrones creacionales, estructurales y de comportamiento, junto con principios SOLID y arquitecturas en capas.
Objetivo: Aprender patrones de diseño fundamentales, principios SOLID, arquitecturas de software y mejores prácticas para crear aplicaciones mantenibles y escalables en Python.
Paso 1: ¿Qué son los patrones de diseño?
Los patrones de diseño son soluciones reutilizables a problemas comunes en el diseño de software. No son código específico, sino conceptos y mejores prácticas que puedes adaptar a diferentes situaciones.
# Analogía: Los patrones son como recetas de cocina para programadores
"""
Singleton → Una única instancia global
Factory → Fábrica de objetos
Observer → Sistema de notificaciones
Strategy → Algoritmos intercambiables
Decorator → Añadir funcionalidad dinámicamente
Adapter → Conectar interfaces incompatibles
"""
Paso 2: Patrones creacionales
Singleton - una única instancia
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
print("Inicializando conexión a base de datos...")
self.connection_count = 0
def connect(self):
self.connection_count += 1
return f"Conexión #{self.connection_count} establecida"
# Uso: Siempre obtenemos la misma instancia
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True - misma instancia
print(db1.connect())
print(db2.connect())
Factory Method - creación flexible
from abc import ABC, abstractmethod
class Documento(ABC):
@abstractmethod
def abrir(self):
pass
class PDF(Documento):
def abrir(self):
return "Abriendo documento PDF con Adobe Reader"
class Word(Documento):
def abrir(self):
return "Abriendo documento Word con Microsoft Word"
class DocumentoFactory:
@staticmethod
def crear_documento(tipo, *args):
if tipo == "pdf":
return PDF(*args)
elif tipo == "word":
return Word(*args)
else:
raise ValueError(f"Tipo de documento no soportado: {tipo}")
# Uso de la fábrica
factory = DocumentoFactory()
doc1 = factory.crear_documento("pdf")
doc2 = factory.crear_documento("word")
print(doc1.abrir())
print(doc2.abrir())
Paso 3: Patrones estructurales
Adapter - conectar interfaces incompatibles
# Sistema legacy que queremos adaptar
class SistemaLegacy:
def obtener_datos_legacy(self):
return {"nombre_completo": "Juan Pérez", "edad_años": 30, "correo_e": "[email protected]"}
# Nueva interfaz que esperamos
class NuevoSistema:
def obtener_datos(self):
return {"firstName": "Ana", "lastName": "García", "age": 25, "email": "[email protected]"}
# Adapter que convierte la interfaz legacy a la nueva
class LegacyAdapter:
def __init__(self, legacy_system):
self.legacy_system = legacy_system
def obtener_datos(self):
datos_legacy = self.legacy_system.obtener_datos_legacy()
return {
"firstName": datos_legacy["nombre_completo"].split()[0],
"lastName": datos_legacy["nombre_completo"].split()[1],
"age": datos_legacy["edad_años"],
"email": datos_legacy["correo_e"]
}
# Uso del adapter
legacy = SistemaLegacy()
adapter = LegacyAdapter(legacy)
nuevo_sistema = NuevoSistema()
print("Datos legacy adaptados:", adapter.obtener_datos())
print("Datos nuevos:", nuevo_sistema.obtener_datos())
Decorator - añadir funcionalidad dinámicamente
from abc import ABC, abstractmethod
class Notificador(ABC):
@abstractmethod
def enviar(self, mensaje):
pass
class NotificadorBase(Notificador):
def enviar(self, mensaje):
return f"Enviando mensaje base: {mensaje}"
class DecoratorNotificador(Notificador):
def __init__(self, notificador):
self.notificador = notificador
def enviar(self, mensaje):
return self.notificador.enviar(mensaje)
class NotificadorFacebook(DecoratorNotificador):
def enviar(self, mensaje):
base_result = super().enviar(mensaje)
return f"{base_result} + via Facebook"
class NotificadorSlack(DecoratorNotificador):
def enviar(self, mensaje):
base_result = super().enviar(mensaje)
return f"{base_result} + via Slack"
class NotificadorSMS(DecoratorNotificador):
def enviar(self, mensaje):
base_result = super().enviar(mensaje)
return f"{base_result} + via SMS"
# Uso: Podemos combinar decoradores dinámicamente
notificador = NotificadorBase()
notificador = NotificadorFacebook(notificador)
notificador = NotificadorSlack(notificador)
notificador = NotificadorSMS(notificador)
print(notificador.enviar("¡Hola Mundo!"))
Paso 4: Patrones de comportamiento
Observer - sistema de notificaciones
class Observable:
def __init__(self):
self._observers = []
def agregar_observer(self, observer):
self._observers.append(observer)
def eliminar_observer(self, observer):
self._observers.remove(observer)
def notificar(self, *args, **kwargs):
for observer in self._observers:
observer.actualizar(*args, **kwargs)
class Observer(ABC):
@abstractmethod
def actualizar(self, *args, **kwargs):
pass
# Implementaciones concretas
class Usuario(Observer):
def __init__(self, nombre):
self.nombre = nombre
def actualizar(self, mensaje):
print(f"{self.nombre} recibió: {mensaje}")
class SistemaLogging(Observer):
def actualizar(self, mensaje):
print(f"[LOG] Evento: {mensaje}")
# Uso del patrón Observer
sistema_notificaciones = Observable()
usuario1 = Usuario("Ana")
usuario2 = Usuario("Carlos")
logging = SistemaLogging()
sistema_notificaciones.agregar_observer(usuario1)
sistema_notificaciones.agregar_observer(usuario2)
sistema_notificaciones.agregar_observer(logging)
sistema_notificaciones.notificar("¡Nueva actualización disponible!")
Strategy - algoritmos intercambiables
from abc import ABC, abstractmethod
class EstrategiaPago(ABC):
@abstractmethod
def pagar(self, cantidad):
pass
class PagoTarjeta(EstrategiaPago):
def pagar(self, cantidad):
return f"Pagando ${cantidad} con tarjeta de crédito"
class PagoPayPal(EstrategiaPago):
def pagar(self, cantidad):
return f"Pagando ${cantidad} con PayPal"
class PagoBitcoin(EstrategiaPago):
def pagar(self, cantidad):
return f"Pagando ${cantidad} con Bitcoin"
class CarritoCompra:
def __init__(self):
self.items = []
self.estrategia_pago = None
def agregar_item(self, item, precio):
self.items.append((item, precio))
def total(self):
return sum(precio for _, precio in self.items)
def set_estrategia_pago(self, estrategia):
self.estrategia_pago = estrategia
def checkout(self):
if not self.estrategia_pago:
raise ValueError("Estrategia de pago no configurada")
total = self.total()
return self.estrategia_pago.pagar(total)
# Uso del patrón Strategy
carrito = CarritoCompra()
carrito.agregar_item("Laptop", 1000)
carrito.agregar_item("Mouse", 50)
# Podemos cambiar la estrategia dinámicamente
carrito.set_estrategia_pago(PagoTarjeta())
print(carrito.checkout())
carrito.set_estrategia_pago(PagoPayPal())
print(carrito.checkout())
carrito.set_estrategia_pago(PagoBitcoin())
print(carrito.checkout())
Paso 5: Principios SOLID
Single Responsibility Principle (SRP)
# ❌ Violación del SRP
class UsuarioMalDiseñado:
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
def guardar_en_bd(self):
# Lógica de persistencia
print(f"Guardando {self.nombre} en base de datos")
def enviar_email(self, mensaje):
# Lógica de notificación
print(f"Enviando email a {self.email}: {mensaje}")
def validar_email(self):
# Lógica de validación
return "@" in self.email
# ✅ Cumpliendo SRP
class Usuario:
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
class UsuarioRepository:
def guardar(self, usuario):
print(f"Guardando {usuario.nombre} en base de datos")
class EmailService:
def enviar(self, usuario, mensaje):
print(f"Enviando email a {usuario.email}: {mensaje}")
class ValidadorUsuario:
@staticmethod
def validar_email(usuario):
return "@" in usuario.email
Open/Closed Principle (OCP)
from abc import ABC, abstractmethod
# ❌ Violación del OCP
class ProcesadorPagos:
def procesar(self, tipo, cantidad):
if tipo == "tarjeta":
return f"Procesando pago con tarjeta: ${cantidad}"
elif tipo == "paypal":
return f"Procesando pago con PayPal: ${cantidad}"
# Debemos modificar la clase para añadir nuevos métodos de pago
# ✅ Cumpliendo OCP
class MetodoPago(ABC):
@abstractmethod
def procesar(self, cantidad):
pass
class TarjetaPago(MetodoPago):
def procesar(self, cantidad):
return f"Procesando pago con tarjeta: ${cantidad}"
class PayPalPago(MetodoPago):
def procesar(self, cantidad):
return f"Procesando pago con PayPal: ${cantidad}"
class BitcoinPago(MetodoPago):
def procesar(self, cantidad):
return f"Procesando pago con Bitcoin: ${cantidad}"
class ProcesadorPagosOCP:
def procesar(self, metodo_pago, cantidad):
return metodo_pago.procesar(cantidad)
# Podemos añadir nuevos métodos de pago sin modificar ProcesadorPagosOCP
Paso 6: Arquitectura en capas
Arquitectura MVC (Model-View-Controller)
# Modelo
class UsuarioModel:
def __init__(self):
self.usuarios = []
def agregar_usuario(self, nombre, email):
self.usuarios.append({"nombre": nombre, "email": email})
def obtener_usuarios(self):
return self.usuarios
# Vista
class UsuarioView:
def mostrar_usuarios(self, usuarios):
print("\n--- Lista de Usuarios ---")
for usuario in usuarios:
print(f"Nombre: {usuario['nombre']}, Email: {usuario['email']}")
# Controlador
class UsuarioController:
def __init__(self):
self.model = UsuarioModel()
self.view = UsuarioView()
def agregar_usuario(self, nombre, email):
self.model.agregar_usuario(nombre, email)
def mostrar_usuarios(self):
usuarios = self.model.obtener_usuarios()
self.view.mostrar_usuarios(usuarios)
# Uso del MVC
controller = UsuarioController()
controller.agregar_usuario("Ana", "[email protected]")
controller.agregar_usuario("Carlos", "[email protected]")
controller.mostrar_usuarios()
Arquitectura hexagonal (ports and adapters)
# Núcleo de la aplicación (sin dependencias externas)
class ServicioUsuarios:
def __init__(self, repositorio):
self.repositorio = repositorio
def registrar_usuario(self, nombre, email):
# Lógica de negocio
if not email or "@" not in email:
raise ValueError("Email inválido")
usuario = {"nombre": nombre, "email": email}
return self.repositorio.guardar(usuario)
# Puerto (interface)
class RepositorioUsuarios(ABC):
@abstractmethod
def guardar(self, usuario):
pass
# Adaptadores
class RepositorioMemoria(RepositorioUsuarios):
def __init__(self):
self.usuarios = []
def guardar(self, usuario):
self.usuarios.append(usuario)
return usuario
class RepositorioBaseDatos(RepositorioUsuarios):
def guardar(self, usuario):
# Conexión real a base de datos aquí
print(f"Guardando en BD: {usuario}")
return usuario
# Podemos cambiar el adaptador sin modificar el núcleo
servicio = ServicioUsuarios(RepositorioMemoria())
usuario = servicio.registrar_usuario("Laura", "[email protected]")
print(f"Usuario registrado: {usuario}")
Paso 7: Testing y calidad de código
Unit testing con unittest
import unittest
class Calculadora:
def sumar(self, a, b):
return a + b
def restar(self, a, b):
return a - b
def multiplicar(self, a, b):
return a * b
def dividir(self, a, b):
if b == 0:
raise ValueError("No se puede dividir por cero")
return a / b
class TestCalculadora(unittest.TestCase):
def setUp(self):
self.calc = Calculadora()
def test_sumar(self):
self.assertEqual(self.calc.sumar(2, 3), 5)
self.assertEqual(self.calc.sumar(-1, 1), 0)
def test_restar(self):
self.assertEqual(self.calc.restar(5, 3), 2)
self.assertEqual(self.calc.restar(0, 5), -5)
def test_multiplicar(self):
self.assertEqual(self.calc.multiplicar(3, 4), 12)
self.assertEqual(self.calc.multiplicar(0, 5), 0)
def test_dividir(self):
self.assertEqual(self.calc.dividir(10, 2), 5)
self.assertAlmostEqual(self.calc.dividir(1, 3), 0.333, places=3)
with self.assertRaises(ValueError):
self.calc.dividir(5, 0)
if __name__ == "__main__":
unittest.main()
Test-Driven Development (TDD)
# Ejemplo de TDD: Desarrollamos una función usando tests primero
# Paso 1: Escribimos el test (fallará)
def test_contar_vocales():
assert contar_vocales("hello") == 2
assert contar_vocales("WORLD") == 1
assert contar_vocales("") == 0
assert contar_vocales("aeiou") == 5
print("Todos los tests pasaron!")
# Paso 2: Implementamos la función mínima para que el test pase
def contar_vocales(texto):
vocales = "aeiouAEIOU"
return sum(1 for char in texto if char in vocales)
# Paso 3: Ejecutamos el test
try:
test_contar_vocales()
print("✅ Tests pasaron exitosamente!")
except AssertionError as e:
print(f"❌ Test falló: {e}")
Paso 8: Proyecto completo - sistema de e-commerce con patrones
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
@dataclass
class Producto:
id: int
nombre: str
precio: float
stock: int
class ObservadorCarrito(ABC):
@abstractmethod
def actualizar(self, carrito):
pass
class CarritoCompra:
def __init__(self):
self.items = []
self.observadores = []
def agregar_observador(self, observador):
self.observadores.append(observador)
def notificar_observadores(self):
for observador in self.observadores:
observador.actualizar(self)
def agregar_producto(self, producto, cantidad=1):
if producto.stock >= cantidad:
self.items.append({"producto": producto, "cantidad": cantidad})
producto.stock -= cantidad
self.notificar_observadores()
else:
raise ValueError("Stock insuficiente")
def calcular_total(self):
return sum(item["producto"].precio * item["cantidad"] for item in self.items)
def vaciar(self):
for item in self.items:
item["producto"].stock += item["cantidad"]
self.items = []
self.notificar_observadores()
class ObservadorStock(ObservadorCarrito):
def actualizar(self, carrito):
print("🔄 Actualizando niveles de stock...")
class ObservadorTotal(ObservadorCarrito):
def actualizar(self, carrito):
total = carrito.calcular_total()
print(f"💰 Total actual del carrito: ${total:.2f}")
# Factory para crear productos
class ProductoFactory:
@staticmethod
def crear_producto(tipo, *args):
if tipo == "electronico":
return Producto(*args, stock=50)
elif tipo == "ropa":
return Producto(*args, stock=100)
elif tipo == "libro":
return Producto(*args, stock=200)
else:
raise ValueError("Tipo de producto no válido")
# Uso del sistema
factory = ProductoFactory()
# Crear productos
laptop = factory.crear_producto("electronico", 1, "Laptop Gaming", 1200.00)
camiseta = factory.crear_producto("ropa", 2, "Camiseta Algodón", 25.00)
libro = factory.crear_producto("libro", 3, "Clean Code", 35.00)
# Configurar carrito con observadores
carrito = CarritoCompra()
carrito.agregar_observador(ObservadorStock())
carrito.agregar_observador(ObservadorTotal())
# Operaciones
carrito.agregar_producto(laptop, 1)
carrito.agregar_producto(camiseta, 2)
carrito.agregar_producto(libro, 1)
print(f"Stock laptop después de compra: {laptop.stock}")
print(f"Stock camiseta después de compra: {camiseta.stock}")
# Strategy para descuentos
class EstrategiaDescuento(ABC):
@abstractmethod
def aplicar_descuento(self, total):
pass
class DescuentoPorcentaje(EstrategiaDescuento):
def __init__(self, porcentaje):
self.porcentaje = porcentaje
def aplicar_descuento(self, total):
return total * (1 - self.porcentaje / 100)
class DescuentoFijo(EstrategiaDescuento):
def __init__(self, cantidad):
self.cantidad = cantidad
def aplicar_descuento(self, total):
return max(0, total - self.cantidad)
# Aplicar descuento
descuento = DescuentoPorcentaje(10) # 10% de descuento
total_con_descuento = descuento.aplicar_descuento(carrito.calcular_total())
print(f"Total con descuento: ${total_con_descuento:.2f}")
Paso 9: Ejercicios de práctica
# Ejercicio 1: Implementar el patrón Command
class Comando(ABC):
@abstractmethod
def ejecutar(self):
pass
@abstractmethod
def deshacer(self):
pass
class ComandoEncenderLuz(Comando):
def __init__(self, luz):
self.luz = luz
def ejecutar(self):
self.luz.encender()
def deshacer(self):
self.luz.apagar()
class Luz:
def __init__(self, nombre):
self.nombre = nombre
self.encendida = False
def encender(self):
self.encendida = True
print(f"{self.nombre}: Luz encendida")
def apagar(self):
self.encendida = False
print(f"{self.nombre}: Luz apagada")
# Ejercicio 2: Implementar el patrón State
class EstadoDocumento(ABC):
@abstractmethod
def publicar(self, documento):
pass
@abstractmethod
def renderizar(self, documento):
pass
class EstadoBorrador(EstadoDocumento):
def publicar(self, documento):
documento.estado = EstadoPublicado()
print("Documento publicado desde borrador")
def renderizar(self, documento):
print("Renderizando documento en modo borrador")
class EstadoPublicado(EstadoDocumento):
def publicar(self, documento):
print("El documento ya está publicado")
def renderizar(self, documento):
print("Renderizando documento en modo público")
class Documento:
def __init__(self, contenido):
self.contenido = contenido
self.estado = EstadoBorrador()
def publicar(self):
self.estado.publicar(self)
def renderizar(self):
self.estado.renderizar(self)
# Probamos el patrón State
doc = Documento("Contenido del documento")
doc.renderizar()
doc.publicar()
doc.renderizar()
doc.publicar()
Paso 10: Próximos pasos en patrones de diseño
Temas para profundizar:
- Patrones avanzados: Command, State, Template Method
- Patrones de arquitectura: Microservicios, CQRS, Event Sourcing
- Testing avanzado: Mocks, stubs, integration tests
- DevOps y CI/CD: Automatización de despliegues
Paso 11: Recursos y herramientas
Recursos para aprender más:
- Design Patterns: Gang of Four book
- Clean Architecture: Robert C. Martin
- Refactoring: Martin Fowler
- SOLID Principles: Uncle Bob
Plataformas de práctica:
- Refactoring Guru: Patrones de diseño interactivos
- SourceMaking: Ejemplos de patrones
- Pluralsight: Cursos avanzados
Herramientas de análisis:
- pylint: Análisis estático de código
- black: Formateador automático
- mypy: Chequeo de tipos estático
- pytest: Framework de testing avanzado
Conclusión
¡Felicidades! Has dominado los fundamentos de los patrones de diseño y arquitectura de software. Practica estos conceptos en proyectos reales y aplica los principios SOLID para crear software mantenible.
Para más tutoriales sobre patrones de diseño avanzados y arquitectura de software, visita nuestra sección de tutoriales.
¡Con estos conocimientos ya puedes crear aplicaciones bien estructuradas y escalables!
💡 Tip Importante
📝 Mejores Prácticas en Patrones de Diseño y Arquitectura
Para aplicar efectivamente los patrones de diseño y principios de arquitectura, considera estos consejos esenciales:
- Elige el patrón correcto: Estudia el problema antes de seleccionar un patrón
- No abuses de los patrones: Usa patrones solo cuando realmente solucionen un problema
- Mantén la simplicidad: Un patrón simple es mejor que uno complejo innecesario
- Documenta tus decisiones: Explica por qué elegiste ciertos patrones en tu código
- Refactoriza gradualmente: Mejora la arquitectura de tu código con el tiempo
- Aprende de ejemplos reales: Estudia código de proyectos open source exitosos
- Combina patrones: Los patrones funcionan mejor cuando se usan juntos
- Prueba tus diseños: Asegúrate de que tus patrones funcionen correctamente
📚 Documentación: Revisa la documentación completa de patrones de diseño en Refactoring Guru y principios SOLID en Clean Code
¡Estos consejos te ayudarán a crear software bien diseñado y mantenible!
No hay comentarios aún
Sé el primero en comentar este tutorial.