
Principios SOLID de Diseño de Software
Aprende los principios SOLID de diseño de software (SRP, OCP, LSP, ISP, DIP) con ejemplos prácticos en Python y mejora la calidad de tu código.
¡Domina los principios SOLID de diseño de software! En este tutorial especializado te guiaré paso a paso para que aprendas los cinco principios fundamentales que te ayudarán a crear software mantenible, extensible y de alta calidad, con ejemplos prácticos en Python.
Objetivo: Aprender y aplicar los principios SOLID (SRP, OCP, LSP, ISP, DIP) en el diseño de software, entender sus beneficios, y saber cuándo y cómo aplicarlos en proyectos reales.
Índice
- Paso 1: ¿Qué son los principios SOLID?
- Paso 2: SRP - Principio de Responsabilidad Única
- Paso 3: OCP - Principio Abierto/Cerrado
- Paso 4: LSP - Principio de Sustitución de Liskov
- Paso 5: ISP - Principio de Segregación de Interfaces
- Paso 6: DIP - Principio de Inversión de Dependencias
- Paso 7: Combinando principios SOLID
- Paso 8: Refactoring aplicando SOLID
- Paso 9: Cuando NO aplicar SOLID
- Conclusión
- 💡 Tip Importante
Paso 1: ¿Qué son los principios SOLID?
Los principios SOLID son cinco principios de diseño orientado a objetos que ayudan a crear software más mantenible, flexible y entendible. Fueron introducidos por Robert C. Martin (Uncle Bob) y se han convertido en estándares fundamentales para el desarrollo de software de calidad.
Los 5 principios SOLID
Principio | Significado | Descripción |
---|---|---|
SRP | Single Responsibility Principle | Una clase debe tener una sola razón para cambiar |
OCP | Open/Closed Principle | Las clases deben estar abiertas para extensión pero cerradas para modificación |
LSP | Liskov Substitution Principle | Las subclases deben ser sustituibles por sus clases base |
ISP | Interface Segregation Principle | Muchas interfaces específicas son mejores que una interfaz general |
DIP | Dependency Inversion Principle | Depender de abstracciones, no de implementaciones concretas |
Beneficios de aplicar SOLID
- Mantenibilidad: Código más fácil de entender y modificar
- Extensibilidad: Fácil agregar nuevas funcionalidades
- Testabilidad: Código más fácil de probar unitariamente
- Reusabilidad: Componentes que pueden ser reutilizados
- Flexibilidad: Fácil adaptación a cambios de requisitos
Paso 2: SRP - Principio de Responsabilidad Única
El Principio de Responsabilidad Única establece que una clase debe tener una y solo una razón para cambiar. Esto significa que una clase debe ocuparse de una única funcionalidad o responsabilidad.
Ejemplo violando SRP
class Usuario:
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
def guardar_en_bd(self):
# Lógica para guardar en base de datos
print(f"Guardando {self.nombre} en BD")
def enviar_email(self, mensaje):
# Lógica para enviar email
print(f"Enviando email a {self.email}: {mensaje}")
def validar_email(self):
# Lógica de validación de email
if "@" not in self.email:
raise ValueError("Email inválido")
return True
# ❌ Problema: La clase Usuario tiene múltiples responsabilidades:
# 1. Representar datos de usuario
# 2. Persistencia en BD
# 3. Envío de emails
# 4. Validación de emails
Refactorizando aplicando SRP
# ✅ Aplicando SRP: Separar responsabilidades en clases distintas
class Usuario:
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
class ValidadorUsuario:
@staticmethod
def validar_email(email):
if "@" not in email:
raise ValueError("Email inválido")
return True
@staticmethod
def validar_nombre(nombre):
if len(nombre.strip()) == 0:
raise ValueError("Nombre no puede estar vacío")
return True
class RepositorioUsuario:
def guardar(self, usuario):
# Lógica para guardar en base de datos
print(f"Guardando {usuario.nombre} en BD")
def obtener_por_id(self, id):
# Lógica para obtener usuario
pass
class ServicioEmail:
def enviar(self, destinatario, mensaje):
# Lógica para enviar email
print(f"Enviando email a {destinatario}: {mensaje}")
# Uso
usuario = Usuario("Ana García", "[email protected]")
validador = ValidadorUsuario()
validador.validar_email(usuario.email)
validador.validar_nombre(usuario.nombre)
repositorio = RepositorioUsuario()
repositorio.guardar(usuario)
servicio_email = ServicioEmail()
servicio_email.enviar(usuario.email, "Bienvenida al sistema")
Casos de uso comunes para SRP
- Separar lógica de negocio de persistencia
- Separar validación de modelos de datos
- Separar lógica de presentación de lógica de aplicación
- Separar configuración de ejecución
Paso 3: OCP - Principio Abierto/Cerrado
El Principio Abierto/Cerrado establece que las clases deben estar abiertas para extensión pero cerradas para modificación. Podemos agregar nuevas funcionalidades sin modificar el código existente.
Ejemplo violando OCP
class ProcesadorPagos:
def procesar_pago(self, metodo, monto):
if metodo == "tarjeta":
return self._procesar_tarjeta(monto)
elif metodo == "paypal":
return self._procesar_paypal(monto)
elif metodo == "bitcoin":
return self._procesar_bitcoin(monto)
else:
raise ValueError("Método de pago no soportado")
def _procesar_tarjeta(self, monto):
return f"Procesando ${monto} con tarjeta"
def _procesar_paypal(self, monto):
return f"Procesando ${monto} con PayPal"
def _procesar_bitcoin(self, monto):
return f"Procesando ${monto} con Bitcoin"
# ❌ Problema: Cada vez que agregamos un nuevo método de pago,
# tenemos que modificar la clase ProcesadorPagos
Refactorizando aplicando OCP
from abc import ABC, abstractmethod
# ✅ Aplicando OCP: Usar abstracciones y herencia
class MetodoPago(ABC):
@abstractmethod
def procesar(self, monto) -> str:
pass
class TarjetaCredito(MetodoPago):
def procesar(self, monto) -> str:
return f"Procesando ${monto} con tarjeta de crédito"
class PayPal(MetodoPago):
def procesar(self, monto) -> str:
return f"Procesando ${monto} con PayPal"
class Bitcoin(MetodoPago):
def procesar(self, monto) -> str:
return f"Procesando ${monto} with Bitcoin"
class ProcesadorPagos:
def procesar_pago(self, metodo: MetodoPago, monto):
return metodo.procesar(monto)
# Ahora podemos agregar nuevos métodos de pago SIN modificar ProcesadorPagos
class TransferenciaBancaria(MetodoPago):
def procesar(self, monto) -> str:
return f"Procesando ${monto} con transferencia bancaria"
# Uso
procesador = ProcesadorPagos()
metodos = [
TarjetaCredito(),
PayPal(),
Bitcoin(),
TransferenciaBancaria() # ✅ Nuevo método sin modificar existentes
]
monto = 100.50
for metodo in metodos:
resultado = procesador.procesar_pago(metodo, monto)
print(resultado)
Estrategias para aplicar OCP
- Usar herencia y polimorfismo
- Implementar patrones Strategy o Template Method
- Usar composición sobre herencia
- Crear interfaces abstractas
Paso 4: LSP - Principio de Sustitución de Liskov
El Principio de Sustitución de Liskov establece que los objetos de una superclase deben ser reemplazables por objetos de una subclase sin alterar las propiedades del programa.
Ejemplo violando LSP
class Rectangulo:
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def calcular_area(self):
return self.ancho * self.alto
class Cuadrado(Rectangulo):
def __init__(self, lado):
super().__init__(lado, lado)
# ❌ Violación LSP: Cuadrado cambia el comportamiento esperado
@property
def ancho(self):
return self._ancho
@ancho.setter
def ancho(self, value):
self._ancho = value
self._alto = value # Forzar que alto sea igual a ancho
@property
def alto(self):
return self._alto
@alto.setter
def alto(self, value):
self._alto = value
self._ancho = value # Forzar que ancho sea igual a alto
def probar_rectangulo(rect: Rectangulo):
rect.ancho = 5
rect.alto = 4
area_esperada = 20
area_real = rect.calcular_area()
if area_real != area_esperada:
raise ValueError(f"LSP violado: Esperado {area_esperada}, obtenido {area_real}")
print("✅ LSP cumplido")
# Uso que demuestra la violación
try:
rectangulo = Rectangulo(5, 4)
probar_rectangulo(rectangulo) # ✅ Funciona
cuadrado = Cuadrado(5)
probar_rectangulo(cuadrado) # ❌ Falla - LSP violado
except ValueError as e:
print(e)
Refactorizando aplicando LSP
from abc import ABC, abstractmethod
# ✅ Aplicando LSP: Diseñar jerarquías correctas
class Forma(ABC):
@abstractmethod
def calcular_area(self):
pass
class Rectangulo(Forma):
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
def calcular_area(self):
return self.ancho * self.alto
class Cuadrado(Forma):
def __init__(self, lado):
self.lado = lado
def calcular_area(self):
return self.lado * self.lado
# Ahora ambas clases implementan Forma correctamente
def probar_area(forma: Forma, area_esperada):
area_real = forma.calcular_area()
if abs(area_real - area_esperada) > 0.001:
raise ValueError(f"Área incorrecta: Esperado {area_esperada}, obtenido {area_real}")
print(f"✅ Área correcta: {area_real}")
# Uso
rectangulo = Rectangulo(5, 4)
cuadrado = Cuadrado(5)
probar_area(rectangulo, 20) # ✅
probar_area(cuadrado, 25) # ✅
# Ambas formas son sustituibles y se comportan correctamente
Reglas para cumplir LSP
- Mismos métodos: Las subclases deben implementar todos los métodos de la superclase
- Mismas precondiciones: Las subclases no deben fortalecer las precondiciones
- Mismas postcondiciones: Las subclases no deben debilitar las postcondiciones
- Mismas invariantes: Las subclases deben preservar las invariantes
Paso 5: ISP - Principio de Segregación de Interfaces
El Principio de Segregación de Interfaces establece que es mejor tener muchas interfaces específicas que una interfaz general. Los clientes no deben verse forzados a depender de interfaces que no usan.
Ejemplo violando ISP
class Dispositivo:
def imprimir(self, documento):
raise NotImplementedError
def escanear(self, documento):
raise NotImplementedError
def fax(self, documento):
raise NotImplementedError
class ImpresoraAntigua(Dispositivo):
def imprimir(self, documento):
print(f"Imprimiendo: {documento}")
def escanear(self, documento):
raise NotImplementedError("Esta impresora no puede escanear")
def fax(self, documento):
raise NotImplementedError("Esta impresora no puede fax")
class ImpresoraMultifuncional(Dispositivo):
def imprimir(self, documento):
print(f"Imprimiendo: {documento}")
def escanear(self, documento):
print(f"Escaneando: {documento}")
def fax(self, documento):
print(f"Enviando fax: {documento}")
# ❌ Problema: ImpresoraAntigua debe implementar métodos que no usa
Refactorizando aplicando ISP
from abc import ABC, abstractmethod
# ✅ Aplicando ISP: Interfaces específicas y pequeñas
class Imprimible(ABC):
@abstractmethod
def imprimir(self, documento):
pass
class Escaneable(ABC):
@abstractmethod
def escanear(self, documento):
pass
class Faxeable(ABC):
@abstractmethod
def fax(self, documento):
pass
class ImpresoraAntigua(Imprimible):
def imprimir(self, documento):
print(f"Imprimiendo: {documento}")
class Escaner(Escaneable):
def escanear(self, documento):
print(f"Escaneando: {documento}")
class ImpresoraMultifuncional(Imprimible, Escaneable, Faxeable):
def imprimir(self, documento):
print(f"Imprimiendo: {documento}")
def escanear(self, documento):
print(f"Escaneando: {documento}")
def fax(self, documento):
print(f"Enviando fax: {documento}")
# Ahora cada clase sólo implementa lo que necesita
def usar_impresora(impresora: Imprimible, documento):
impresora.imprimir(documento)
def usar_escaner(escaner: Escaneable, documento):
escaner.escanear(documento)
# Uso
impresora_vieja = ImpresoraAntigua()
impresora_multifuncional = ImpresoraMultifuncional()
escaner = Escaner()
documento = "Mi documento importante"
usar_impresora(impresora_vieja, documento)
usar_impresora(impresora_multifuncional, documento)
usar_escaner(escaner, documento)
usar_escaner(impresora_multifuncional, documento) # ✅ También funciona
Beneficios de ISP
- Menos acoplamiento: Clientes dependen sólo de lo que necesitan
- Mejor mantenibilidad: Cambios en una interfaz afectan menos código
- Mayor cohesión: Interfaces más enfocadas y específicas
- Mejor testing: Más fácil mockear interfaces pequeñas
Paso 6: DIP - Principio de Inversión de Dependencias
El Principio de Inversión de Dependencias establece que los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
Ejemplo violando DIP
# Módulos de bajo nivel
class BaseDatosMySQL:
def guardar(self, datos):
print(f"Guardando en MySQL: {datos}")
def obtener(self, id):
print(f"Obteniendo de MySQL: {id}")
class ServicioEmail:
def enviar(self, destinatario, mensaje):
print(f"Enviando email a {destinatario}: {mensaje}")
# Módulo de alto nivel
class ServicioUsuarios:
def __init__(self):
self.db = BaseDatosMySQL() # ❌ Dependencia concreta
self.email = ServicioEmail() # ❌ Dependencia concreta
def registrar_usuario(self, nombre, email):
# Lógica de negocio
usuario = {"nombre": nombre, "email": email}
# Depende directamente de implementaciones concretas
self.db.guardar(usuario)
self.email.enviar(email, "Bienvenido al sistema")
return usuario
# ❌ Problema: ServicioUsuarios está fuertemente acoplado a implementaciones específicas
Refactorizando aplicando DIP
from abc import ABC, abstractmethod
# ✅ Aplicando DIP: Depender de abstracciones
# Abstracciones (interfaces)
class Repositorio(ABC):
@abstractmethod
def guardar(self, datos):
pass
@abstractmethod
def obtener(self, id):
pass
class ServicioNotificaciones(ABC):
@abstractmethod
def enviar(self, destinatario, mensaje):
pass
# Implementaciones concretas de bajo nivel
class BaseDatosMySQL(Repositorio):
def guardar(self, datos):
print(f"Guardando en MySQL: {datos}")
def obtener(self, id):
print(f"Obteniendo de MySQL: {id}")
return {"id": id, "nombre": "Usuario Ejemplo"}
class BaseDatosPostgreSQL(Repositorio):
def guardar(self, datos):
print(f"Guardando en PostgreSQL: {datos}")
def obtener(self, id):
print(f"Obteniendo de PostgreSQL: {id}")
return {"id": id, "nombre": "Usuario Ejemplo"}
class ServicioEmail(ServicioNotificaciones):
def enviar(self, destinatario, mensaje):
print(f"Enviando email a {destinatario}: {mensaje}")
class ServicioSMS(ServicioNotificaciones):
def enviar(self, destinatario, mensaje):
print(f"Enviando SMS a {destinatario}: {mensaje}")
# Módulo de alto nivel
class ServicioUsuarios:
def __init__(self, repositorio: Repositorio, notificador: ServicioNotificaciones):
self.repositorio = repositorio # ✅ Dependencia abstracta
self.notificador = notificador # ✅ Dependencia abstracta
def registrar_usuario(self, nombre, email):
usuario = {"nombre": nombre, "email": email}
self.repositorio.guardar(usuario)
self.notificador.enviar(email, "Bienvenido al sistema")
return usuario
# Configuración de dependencias (Inyección de Dependencias)
def configurar_aplicacion():
# Podemos cambiar fácilmente las implementaciones
repositorio = BaseDatosPostgreSQL() # o BaseDatosMySQL()
notificador = ServicioSMS() # o ServicioEmail()
return ServicioUsuarios(repositorio, notificador)
# Uso
servicio = configurar_aplicacion()
usuario = servicio.registrar_usuario("Ana García", "[email protected]")
print(f"Usuario registrado: {usuario}")
Ventajas de DIP
- Desacoplamiento: Módulos independientes y reutilizables
- Flexibilidad: Fácil cambiar implementaciones
- Testabilidad: Fácil usar mocks en tests
- Mantenibilidad: Cambios aislados y controlados
Paso 7: Combinando principios SOLID
Los principios SOLID funcionan mejor cuando se aplican en conjunto. Veamos un ejemplo que combina múltiples principios.
Sistema de procesamiento de pedidos
from abc import ABC, abstractmethod
from typing import List, Dict, Any
import uuid
# DIP: Abstracciones
class RepositorioPedidos(ABC):
@abstractmethod
def guardar(self, pedido) -> Any:
pass
@abstractmethod
def obtener_por_id(self, id: str) -> Any:
pass
class ServicioNotificaciones(ABC):
@abstractmethod
def enviar(self, destinatario: str, mensaje: str):
pass
class CalculadorDescuentos(ABC):
@abstractmethod
def calcular_descuento(self, total: float) -> float:
pass
# ISP: Interfaces específicas
class ValidadorPedido(ABC):
@abstractmethod
def validar(self, pedido) -> bool:
pass
# Implementaciones concretas
class RepositorioPedidosMySQL(RepositorioPedidos):
def guardar(self, pedido) -> Any:
print(f"Guardando pedido {pedido['id']} en MySQL")
return pedido
def obtener_por_id(self, id: str) -> Any:
print(f"Obteniendo pedido {id} desde MySQL")
return {"id": id, "estado": "procesado"}
class ServicioEmail(ServicioNotificaciones):
def enviar(self, destinatario: str, mensaje: str):
print(f"Enviando email a {destinatario}: {mensaje}")
class CalculadorDescuentosEstándar(CalculadorDescuentos):
def calcular_descuento(self, total: float) -> float:
if total > 1000:
return total * 0.1 # 10% de descuento
elif total > 500:
return total * 0.05 # 5% de descuento
return 0
class ValidadorStock(ValidadorPedido):
def __init__(self, inventario: Dict[str, int]):
self.inventario = inventario
def validar(self, pedido) -> bool:
for item in pedido['items']:
producto_id = item['producto_id']
cantidad = item['cantidad']
if self.inventario.get(producto_id, 0) < cantidad:
raise ValueError(f"Stock insuficiente para producto {producto_id}")
return True
# SRP: Cada clase tiene una responsabilidad clara
class ProcesadorPedidos:
def __init__(self,
repositorio: RepositorioPedidos,
notificador: ServicioNotificaciones,
calculador_descuentos: CalculadorDescuentos,
validadores: List[ValidadorPedido]):
self.repositorio = repositorio
self.notificador = notificador
self.calculador_descuentos = calculador_descuentos
self.validadores = validadores
# OCP: Fácil extender con nuevos validadores
def agregar_validador(self, validador: ValidadorPedido):
self.validadores.append(validador)
def procesar_pedido(self, usuario: Dict[str, Any], items: List[Dict[str, Any]]) -> Dict[str, Any]:
# Crear pedido
pedido = {
'id': str(uuid.uuid4()),
'usuario': usuario,
'items': items,
'estado': 'pendiente'
}
# Validar pedido (OCP: nuevos validadores se agregan fácilmente)
for validador in self.validadores:
validador.validar(pedido)
# Calcular total
total = sum(item['precio'] * item['cantidad'] for item in items)
descuento = self.calculador_descuentos.calcular_descuento(total)
total_con_descuento = total - descuento
pedido['total'] = total
pedido['descuento'] = descuento
pedido['total_con_descuento'] = total_con_descuento
pedido['estado'] = 'procesado'
# Guardar y notificar
pedido_guardado = self.repositorio.guardar(pedido)
self.notificador.enviar(usuario['email'], f"Pedido {pedido['id']} procesado. Total: ${total_con_descuento:.2f}")
return pedido_guardado
# Configuración (DIP: Inyección de dependencias)
def configurar_sistema():
inventario = {"prod1": 10, "prod2": 5, "prod3": 20}
repositorio = RepositorioPedidosMySQL()
notificador = ServicioEmail()
calculador_descuentos = CalculadorDescuentosEstándar()
validador_stock = ValidadorStock(inventario)
procesador = ProcesadorPedidos(repositorio, notificador, calculador_descuentos, [validador_stock])
return procesador
# Uso
sistema = configurar_sistema()
usuario = {"id": "user1", "nombre": "Ana", "email": "[email protected]"}
items = [
{"producto_id": "prod1", "nombre": "Laptop", "precio": 1200, "cantidad": 1},
{"producto_id": "prod2", "nombre": "Mouse", "precio": 45, "cantidad": 2}
]
try:
pedido = sistema.procesar_pedido(usuario, items)
print(f"Pedido procesado exitosamente: {pedido['id']}")
print(f"Total con descuento: ${pedido['total_con_descuento']:.2f}")
except ValueError as e:
print(f"Error procesando pedido: {e}")
Paso 8: Refactoring aplicando SOLID
Veamos un ejemplo práctico de refactorización aplicando todos los principios SOLID.
Código original con problemas SOLID
class TiendaOnline:
def __init__(self):
self.productos = []
self.usuarios = []
def agregar_producto(self, nombre, precio):
self.productos.append({"nombre": nombre, "precio": precio})
def registrar_usuario(self, nombre, email):
self.usuarios.append({"nombre": nombre, "email": email})
def calcular_total_carrito(self, items):
total = 0
for item in items:
total += item["precio"] * item["cantidad"]
if total > 100:
total *= 0.9 # Descuento
return total
def procesar_pago(self, total, metodo_pago):
if metodo_pago == "tarjeta":
return f"Procesando ${total} con tarjeta"
elif metodo_pago == "paypal":
return f"Procesando ${total} con PayPal"
else:
raise ValueError("
Método de pago no soportado"
def guardar_en_bd(self, datos):
# Lógica de base de datos
print(f"Guardando en BD: {datos}")
def enviar_email(self, destinatario, mensaje):
# Lógica de email
print(f"Enviando email a {destinatario}: {mensaje}")
# ❌ Problemas SOLID:
# - SRP: Múltiples responsabilidades en una clase
# - OCP: Difícil agregar nuevos métodos de pago
# - DIP: Dependencias concretas
Refactorización aplicando SOLID
from abc import ABC, abstractmethod
from typing import List, Dict, Any
# SRP: Modelos separados
class Producto:
def __init__(self, nombre: str, precio: float):
self.nombre = nombre
self.precio = precio
class Usuario:
def __init__(self, nombre: str, email: str):
self.nombre = nombre
self.email = email
# DIP: Abstracciones
class Repositorio(ABC):
@abstractmethod
def guardar(self, datos: Any) -> Any:
pass
class ServicioNotificaciones(ABC):
@abstractmethod
def enviar(self, destinatario: str, mensaje: str):
pass
class MetodoPago(ABC):
@abstractmethod
def procesar(self, monto: float) -> str:
pass
class CalculadorDescuentos(ABC):
@abstractmethod
def calcular_descuento(self, total: float) -> float:
pass
# ISP: Interfaces específicas
class Validador(ABC):
@abstractmethod
def validar(self, datos: Any) -> bool:
pass
# Implementaciones concretas
class RepositorioMySQL(Repositorio):
def guardar(self, datos: Any) -> Any:
print(f"Guardando en MySQL: {datos}")
return datos
class ServicioEmail(ServicioNotificaciones):
def enviar(self, destinatario: str, mensaje: str):
print(f"Enviando email a {destinatario}: {mensaje}")
class TarjetaCredito(MetodoPago):
def procesar(self, monto: float) -> str:
return f"Procesando ${monto} con tarjeta de crédito"
class PayPal(MetodoPago):
def procesar(self, monto: float) -> str:
return f"Procesando ${monto} con PayPal"
class CalculadorDescuentosEstándar(CalculadorDescuentos):
def calcular_descuento(self, total: float) -> float:
if total > 100:
return total * 0.1
return 0
class ValidadorStock(Validador):
def __init__(self, inventario: Dict[str, int]):
self.inventario = inventario
def validar(self, items: List[Dict[str, Any]]) -> bool:
for item in items:
if item["cantidad"] > self.inventario.get(item["producto_id"], 0):
raise ValueError(f"Stock insuficiente para {item['producto_id']}")
return True
# OCP: Fácil extensión
class TiendaOnline:
def __init__(self,
repositorio: Repositorio,
notificador: ServicioNotificaciones,
calculador_descuentos: CalculadorDescuentos,
validadores: List[Validador]):
self.repositorio = repositorio
self.notificador = notificador
self.calculador_descuentos = calculador_descuentos
self.validadores = validadores
self.metodos_pago = {}
def registrar_metodo_pago(self, nombre: str, metodo: MetodoPago):
self.metodos_pago[nombre] = metodo
def agregar_validador(self, validador: Validador):
self.validadores.append(validador)
def procesar_compra(self, usuario: Usuario, items: List[Dict[str, Any]], metodo_pago: str) -> Dict[str, Any]:
# Validar
for validador in self.validadores:
validador.validar(items)
# Calcular total
total = sum(item["precio"] * item["cantidad"] for item in items)
descuento = self.calculador_descuentos.calcular_descuento(total)
total_final = total - descuento
# Procesar pago
if metodo_pago not in self.metodos_pago:
raise ValueError(f"Método de pago no soportado: {metodo_pago}")
resultado_pago = self.metodos_pago[metodo_pago].procesar(total_final)
# Guardar y notificar
compra = {
"usuario": usuario.nombre,
"items": items,
"total": total_final,
"metodo_pago": metodo_pago
}
self.repositorio.guardar(compra)
self.notificador.enviar(usuario.email, f"Compra procesada: ${total_final:.2f}")
return {
"compra": compra,
"resultado_pago": resultado_pago
}
# Configuración
def configurar_tienda():
inventario = {"prod1": 10, "prod2": 5}
repositorio = RepositorioMySQL()
notificador = ServicioEmail()
calculador = CalculadorDescuentosEstándar()
validador_stock = ValidadorStock(inventario)
tienda = TiendaOnline(repositorio, notificador, calculador, [validador_stock])
# Registrar métodos de pago (OCP)
tienda.registrar_metodo_pago("tarjeta", TarjetaCredito())
tienda.registrar_metodo_pago("paypal", PayPal())
return tienda
# Uso
tienda = configurar_tienda()
usuario = Usuario("Ana García", "[email protected]")
items = [
{"producto_id": "prod1", "nombre": "Laptop", "precio": 1200, "cantidad": 1},
{"producto_id": "prod2", "nombre": "Mouse", "precio": 45, "cantidad": 2}
]
try:
resultado = tienda.procesar_compra(usuario, items, "paypal")
print("✅ Compra exitosa:")
print(f" Total: ${resultado['compra']['total']:.2f}")
print(f" {resultado['resultado_pago']}")
except ValueError as e:
print(f"❌ Error: {e}")
Paso 9: Cuando NO aplicar SOLID
Aunque los principios SOLID son importantes, hay situaciones donde su aplicación estricta puede ser contraproducente.
Casos donde aplicar SOLID con cuidado
-
Proyectos pequeños y simples
# ❌ Over-engineering para un script simple class CalculadoraSimple: def sumar(self, a, b): return a + b def restar(self, a, b): return a - b # ✅ Mejor mantenerlo simple
-
Prototipos y pruebas de concepto
# Durante el prototyping, focus en funcionalidad, no en arquitectura class PrototipoRapido: def hacer_todo(self): # Código rápido y sucio para validar idea pass
-
Performance crítica
# En algunos casos, las abstracciones pueden afectar el performance # Evaluar trade-off entre clean code y performance
-
Cuando añade complejidad innecesaria
# Si el beneficio no justifica la complejidad añadida # Mantener el principio KISS (Keep It Simple, Stupid)
Regla general
Aplica SOLID cuando:
- El proyecto tiene vida larga
- Requiere mantenimiento frecuente
- Tiene múltiples desarrolladores
- Necesita alta testabilidad
Considera alternativas cuando:
- Es un proyecto pequeño/script
- Es un prototipo temporal
- El performance es crítico
- La simplicidad es más importante
Conclusión
¡Has dominado los principios SOLID de diseño de software! Estos principios te proporcionan un framework sólido para crear software mantenible, extensible y de alta calidad.
Recuerda que SOLID son guías, no reglas absolutas. La clave está en entender el contexto y aplicar estos principios de manera pragmática.
Practica aplicando estos principios en tus proyectos y siempre evalúa el trade-off entre clean code y simplicidad.
Para más tutoriales sobre principios de diseño y patrones, visita nuestra sección de tutoriales.
¡Sigue practicando y aplicando estos principios en proyectos reales!
💡 Tip Importante
📝 Mejores Prácticas con SOLID
- Empieza simple: No apliques todos los principios desde el inicio
- Refactoriza incrementalmente: Mejora el código existente aplicando SOLID gradualmente
- Entiende el contexto: Aplica SOLID según las necesidades del proyecto
- Mide el impacto: Evalúa si los beneficios justifican la complejidad added
- Mantén el balance: Encuentra el equilibrio entre SOLID y simplicidad
- Aprende de ejemplos: Estudia código bien diseñado en proyectos open source
- Practica constantemente: La maestría viene con la práctica repetida
- Busca feedback: Comparte tu código con otros desarrolladores
📚 Recursos Recomendados:
- Clean Code - Robert C. Martin
- Clean Architecture - Robert C. Martin
- Agile Software Development - Robert C. Martin
- Refactoring.Guru - SOLID - Ejemplos interactivos
- Articulos de Martin Fowler - Sobre arquitectura y diseño
¡Estos principios transformarán la forma en que diseñas y construyes software!
No hay comentarios aún
Sé el primero en comentar este tutorial.