🧑💻 FASE 1: Fundamentos Reforzados - Patrones de Diseño
Recetas prácticas para dominar los fundamentos técnicos del desarrollo de software
📚 Tabla de Contenidos
- Singleton - Una única instancia
- Factory - Creación de objetos flexible
- Observer - Notificación de cambios
- Strategy - Algoritmos intercambiables
- Decorator - Extender funcionalidad dinámicamente
Patrones de Diseño
Receta 2.1: Singleton - Una única instancia
¿Qué es? Patrón que garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a ella.
Caso de uso: Configuración global de aplicación
import threading
from typing import Any, Dict
class ConfigurationManager:
"""Singleton thread-safe para gestión de configuración"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
# Double-check locking
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
# Inicializar solo una vez
if not self._initialized:
self._config: Dict[str, Any] = {
'database': {
'host': 'localhost',
'port': 5432,
'pool_size': 10
},
'cache': {
'ttl': 300,
'max_size': 1000
},
'api': {
'rate_limit': 100,
'timeout': 30
}
}
self._initialized = True
def get(self, key: str, default=None):
"""Obtener valor de configuración con notación dot"""
keys = key.split('.')
value = self._config
for k in keys:
if isinstance(value, dict):
value = value.get(k)
else:
return default
if value is None:
return default
return value
def set(self, key: str, value: Any):
"""Establecer valor de configuración"""
keys = key.split('.')
config = self._config
for k in keys[:-1]:
config = config.setdefault(k, {})
config[keys[-1]] = value
# Demostración
config1 = ConfigurationManager()
config2 = ConfigurationManager()
print(f"Son la misma instancia: {config1 is config2}") # True
print(f"ID config1: {id(config1)}")
print(f"ID config2: {id(config2)}")
# Configurar desde cualquier parte
config1.set('database.host', 'production.db.com')
config1.set('api.rate_limit', 500)
# Acceder desde cualquier parte
print(f"
Database host: {config2.get('database.host')}")
print(f"API rate limit: {config2.get('api.rate_limit')}")
print(f"Cache TTL: {config2.get('cache.ttl')}")
# Simular uso en diferentes módulos
def module_a():
config = ConfigurationManager()
return config.get('database.pool_size')
def module_b():
config = ConfigurationManager()
config.set('database.pool_size', 20)
module_b()
print(f"
Pool size desde module_a: {module_a()}") # 20 Alternativa moderna: Módulo como Singleton
# config.py
class _Config:
def __init__(self):
self.database_url = "postgresql://localhost/mydb"
self.debug = True
self.secret_key = "dev-secret-123"
# Crear instancia única a nivel de módulo
config = _Config()
# En otros archivos, simplemente:
# from config import config
# print(config.database_url) Cuándo usar:
- Logging systems
- Connection pools
- Caches globales
- Configuración de aplicación
Cuándo NO usar:
- Dificulta testing (estado global)
- Puede ocultar dependencias
- Problemas con multi-threading si no se implementa bien
Receta 2.2: Factory - Creación de objetos flexible
¿Qué es? Patrón que delega la creación de objetos a subclases o métodos especializados, permitiendo crear objetos sin especificar la clase exacta.
Caso de uso: Parsers de diferentes formatos
from abc import ABC, abstractmethod
import json
import yaml
import csv
from io import StringIO
class DataParser(ABC):
"""Interfaz base para parsers"""
@abstractmethod
def parse(self, data: str) -> dict:
"""Parsear datos al formato estándar"""
pass
@abstractmethod
def serialize(self, data: dict) -> str:
"""Serializar datos del formato estándar"""
pass
class JSONParser(DataParser):
def parse(self, data: str) -> dict:
return json.loads(data)
def serialize(self, data: dict) -> str:
return json.dumps(data, indent=2)
class YAMLParser(DataParser):
def parse(self, data: str) -> dict:
return yaml.safe_load(data)
def serialize(self, data: dict) -> str:
return yaml.dump(data, default_flow_style=False)
class CSVParser(DataParser):
def parse(self, data: str) -> dict:
"""Convertir CSV a dict con primera fila como keys"""
reader = csv.DictReader(StringIO(data))
return {"rows": list(reader)}
def serialize(self, data: dict) -> str:
"""Convertir dict a CSV"""
if not data.get("rows"):
return ""
output = StringIO()
writer = csv.DictWriter(output, fieldnames=data["rows"][0].keys())
writer.writeheader()
writer.writerows(data["rows"])
return output.getvalue()
class ParserFactory:
"""Factory para crear parsers según el tipo"""
_parsers = {
'json': JSONParser,
'yaml': YAMLParser,
'yml': YAMLParser,
'csv': CSVParser,
}
@classmethod
def create_parser(cls, format_type: str) -> DataParser:
"""Crear parser apropiado"""
parser_class = cls._parsers.get(format_type.lower())
if not parser_class:
raise ValueError(f"Formato no soportado: {format_type}")
return parser_class()
@classmethod
def register_parser(cls, format_type: str, parser_class):
"""Registrar nuevo tipo de parser (extensibilidad)"""
cls._parsers[format_type] = parser_class
# Uso del factory
def process_data_file(filename: str, content: str):
"""Procesar archivo sin saber su formato de antemano"""
# Detectar formato por extensión
extension = filename.split('.')[-1]
# Factory crea el parser apropiado
parser = ParserFactory.create_parser(extension)
# Usar el parser de forma polimórfica
data = parser.parse(content)
print(f"
=== Procesando {filename} ===")
print(f"Tipo de parser: {parser.__class__.__name__}")
print(f"Datos parseados: {data}")
# Serializar de vuelta
serialized = parser.serialize(data)
print(f"Serializado:
{serialized[:100]}...")
# Ejemplos
json_content = '{"name": "John", "age": 30, "city": "NYC"}'
process_data_file("user.json", json_content)
yaml_content = """
name: Jane
age: 25
city: SF
"""
process_data_file("user.yaml", yaml_content)
csv_content = """name,age,city
Bob,35,LA
Alice,28,Chicago"""
process_data_file("users.csv", csv_content)
# Extensibilidad: Agregar nuevo formato
class XMLParser(DataParser):
def parse(self, data: str) -> dict:
# Implementación simplificada
return {"xml": "parsed data"}
def serialize(self, data: dict) -> str:
return "<data>serialized</data>"
ParserFactory.register_parser('xml', XMLParser)
process_data_file("data.xml", "<root><item>test</item></root>") Factory Method vs Abstract Factory:
# Factory Method: Una familia de productos
class NotificationFactory:
@staticmethod
def create(channel: str):
if channel == 'email':
return EmailNotification()
elif channel == 'sms':
return SMSNotification()
elif channel == 'push':
return PushNotification()
# Abstract Factory: Familias completas de productos relacionados
class CloudProviderFactory(ABC):
@abstractmethod
def create_compute(self):
pass
@abstractmethod
def create_storage(self):
pass
class AWSFactory(CloudProviderFactory):
def create_compute(self):
return EC2Instance()
def create_storage(self):
return S3Bucket()
class AzureFactory(CloudProviderFactory):
def create_compute(self):
return VirtualMachine()
def create_storage(self):
return BlobStorage() Receta 2.3: Observer - Notificación de cambios
¿Qué es? Patrón que define una dependencia uno-a-muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados automáticamente.
Caso de uso: Sistema de eventos en una aplicación
from abc import ABC, abstractmethod
from typing import List, Any
from datetime import datetime
class Observer(ABC):
"""Interfaz para observadores"""
@abstractmethod
def update(self, event: str, data: Any):
"""Recibir notificación de cambio"""
pass
class Subject:
"""Sujeto observable"""
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
"""Registrar observador"""
if observer not in self._observers:
self._observers.append(observer)
print(f"✓ {observer.__class__.__name__} attached")
def detach(self, observer: Observer):
"""Eliminar observador"""
self._observers.remove(observer)
print(f"✗ {observer.__class__.__name__} detached")
def notify(self, event: str, data: Any = None):
"""Notificar a todos los observadores"""
print(f"
📢 Notificando evento: {event}")
for observer in self._observers:
observer.update(event, data)
class UserAccount(Subject):
"""Cuenta de usuario observable"""
def __init__(self, username: str):
super().__init__()
self.username = username
self.balance = 0
def deposit(self, amount: float):
"""Depositar dinero"""
self.balance += amount
self.notify('deposit', {
'username': self.username,
'amount': amount,
'new_balance': self.balance,
'timestamp': datetime.now()
})
def withdraw(self, amount: float):
"""Retirar dinero"""
if amount > self.balance:
self.notify('insufficient_funds', {
'username': self.username,
'attempted': amount,
'available': self.balance
})
return False
self.balance -= amount
self.notify('withdrawal', {
'username': self.username,
'amount': amount,
'new_balance': self.balance,
'timestamp': datetime.now()
})
return True
# Observadores concretos
class EmailNotifier(Observer):
"""Envía emails en eventos importantes"""
def update(self, event: str, data: Any):
if event == 'deposit' and data['amount'] >= 1000:
print(f"📧 EMAIL: Large deposit of ${data['amount']:.2f} detected for {data['username']}")
elif event == 'insufficient_funds':
print(f"📧 EMAIL: Insufficient funds alert for {data['username']}")
class SMSNotifier(Observer):
"""Envía SMS para retiros"""
def update(self, event: str, data: Any):
if event == 'withdrawal':
print(f"📱 SMS: ${data['amount']:.2f} withdrawn from your account. New balance: ${data['new_balance']:.2f}")
class AuditLogger(Observer):
"""Registra todas las transacciones"""
def __init__(self):
self.logs = []
def update(self, event: str, data: Any):
log_entry = {
'event': event,
'data': data,
'logged_at': datetime.now()
}
self.logs.append(log_entry)
print(f"📝 AUDIT: Logged {event} event")
class FraudDetector(Observer):
"""Detecta actividad sospechosa"""
def __init__(self):
self.transaction_count = {}
def update(self, event: str, data: Any):
if event in ['deposit', 'withdrawal']:
username = data['username']
self.transaction_count[username] = self.transaction_count.get(username, 0) + 1
if self.transaction_count[username] >= 3:
print(f"🚨 FRAUD ALERT: Multiple transactions ({self.transaction_count[username]}) from {username}")
# Demostración
print("=== Sistema de Observadores ===")
account = UserAccount("john_doe")
# Registrar observadores
email_notifier = EmailNotifier()
sms_notifier = SMSNotifier()
audit_logger = AuditLogger()
fraud_detector = FraudDetector()
account.attach(email_notifier)
account.attach(sms_notifier)
account.attach(audit_logger)
account.attach(fraud_detector)
# Realizar transacciones
account.deposit(500)
account.deposit(1500) # Triggerea email por cantidad grande
account.withdraw(300)
account.withdraw(100)
account.withdraw(50) # Triggerea fraud alert
account.withdraw(5000) # Insufficient funds
# Desregistrar un observador
print("
--- Removiendo SMS Notifier ---")
account.detach(sms_notifier)
account.deposit(200) # Solo los otros observadores se notifican
# Ver logs de auditoría
print(f"
📊 Total de logs de auditoría: {len(audit_logger.logs)}") Aplicaciones reales:
- Event buses (Redux, Vue.js reactivity)
- WebSockets/Real-time updates
- Model-View-Controller (MVC)
- Pub/Sub messaging systems
- Reactive programming (RxJS, React hooks)
Receta 2.4: Strategy - Algoritmos intercambiables
¿Qué es? Patrón que define una familia de algoritmos, encapsula cada uno y los hace intercambiables, permitiendo que el algoritmo varíe independientemente de los clientes que lo usan.
Caso de uso: Sistema de precios con diferentes estrategias
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, date
from typing import List
@dataclass
class Product:
name: str
base_price: float
category: str
@dataclass
class Customer:
name: str
loyalty_level: str # 'bronze', 'silver', 'gold', 'platinum'
member_since: date
class PricingStrategy(ABC):
"""Estrategia base de pricing"""
@abstractmethod
def calculate_price(self, product: Product, customer: Customer, quantity: int) -> float:
"""Calcular precio final"""
pass
@abstractmethod
def get_description(self) -> str:
"""Descripción de la estrategia"""
pass
class RegularPricing(PricingStrategy):
"""Sin descuentos"""
def calculate_price(self, product: Product, customer: Customer, quantity: int) -> float:
return product.base_price * quantity
def get_description(self) -> str:
return "Precio regular sin descuentos"
class LoyaltyPricing(PricingStrategy):
"""Descuentos basados en nivel de lealtad"""
DISCOUNTS = {
'bronze': 0.05, # 5%
'silver': 0.10, # 10%
'gold': 0.15, # 15%
'platinum': 0.20 # 20%
}
def calculate_price(self, product: Product, customer: Customer, quantity: int) -> float:
base_total = product.base_price * quantity
discount = self.DISCOUNTS.get(customer.loyalty_level, 0)
return base_total * (1 - discount)
def get_description(self) -> str:
return "Descuento por nivel de lealtad del cliente"
class BulkPricing(PricingStrategy):
"""Descuentos por volumen"""
def calculate_price(self, product: Product, customer: Customer, quantity: int) -> float:
base_total = product.base_price * quantity
if quantity >= 100:
discount = 0.20 # 20% para 100+
elif quantity >= 50:
discount = 0.15 # 15% para 50+
elif quantity >= 20:
discount = 0.10 # 10% para 20+
else:
discount = 0
return base_total * (1 - discount)
def get_description(self) -> str:
return "Descuento por cantidad (20+ items: 10%, 50+: 15%, 100+: 20%)"
class SeasonalPricing(PricingStrategy):
"""Descuentos por temporada"""
def __init__(self):
self.current_month = datetime.now().month
# Temporada alta: junio-agosto, temporada baja: diciembre-febrero
self.high_season = [6, 7, 8]
self.low_season = [12, 1, 2]
def calculate_price(self, product: Product, customer: Customer, quantity: int) -> float:
base_total = product.base_price * quantity
if self.current_month in self.high_season:
# Precio premium en temporada alta
return base_total * 1.20
elif self.current_month in self.low_season:
# Descuento en temporada baja
return base_total * 0.80
else:
return base_total
def get_description(self) -> str:
return "Ajuste de precios por temporada (+20% alta, -20% baja)"
class ComboStrategy(PricingStrategy):
"""Combina múltiples estrategias tomando el mejor precio"""
def __init__(self, strategies: List[PricingStrategy]):
self.strategies = strategies
def calculate_price(self, product: Product, customer: Customer, quantity: int) -> float:
# Aplicar todas las estrategias y tomar el precio más bajo
prices = [
strategy.calculate_price(product, customer, quantity)
for strategy in self.strategies
]
return min(prices)
def get_description(self) -> str:
strategy_names = [s.__class__.__name__ for s in self.strategies]
return f"Mejor precio entre: {', '.join(strategy_names)}"
class ShoppingCart:
"""Carrito con estrategia de pricing intercambiable"""
def __init__(self, customer: Customer, pricing_strategy: PricingStrategy):
self.customer = customer
self.pricing_strategy = pricing_strategy
self.items: List[tuple] = [] # (product, quantity)
def add_item(self, product: Product, quantity: int):
"""Agregar producto al carrito"""
self.items.append((product, quantity))
def set_pricing_strategy(self, strategy: PricingStrategy):
"""Cambiar estrategia de pricing"""
self.pricing_strategy = strategy
print(f"
🔄 Estrategia cambiada a: {strategy.get_description()}")
def calculate_total(self) -> float:
"""Calcular total con la estrategia actual"""
total = 0
for product, quantity in self.items:
item_price = self.pricing_strategy.calculate_price(product, self.customer, quantity)
total += item_price
return total
def show_receipt(self):
"""Mostrar recibo detallado"""
print(f"
{'='*60}")
print(f"Cliente: {self.customer.name} ({self.customer.loyalty_level.upper()})")
print(f"Estrategia: {self.pricing_strategy.get_description()}")
print(f"{'='*60}")
total = 0
for product, quantity in self.items:
base_total = product.base_price * quantity
final_price = self.pricing_strategy.calculate_price(product, self.customer, quantity)
discount = base_total - final_price
print(f"{product.name:30} x{quantity:3}")
print(f" Base: ${base_total:8.2f} | Final: ${final_price:8.2f} | Ahorro: ${discount:8.2f}")
total += final_price
print(f"{'='*60}")
print(f"TOTAL: ${total:.2f}")
print(f"{'='*60}
")
# Demostración
laptop = Product("Laptop Pro", 1200.00, "electronics")
mouse = Product("Wireless Mouse", 25.00, "accessories")
keyboard = Product("Mechanical Keyboard", 80.00, "accessories")
customer_gold = Customer("Alice", "gold", date(2020, 1, 15))
# Crear carrito con estrategia inicial
cart = ShoppingCart(customer_gold, RegularPricing())
cart.add_item(laptop, 1)
cart.add_item(mouse, 50)
cart.add_item(keyboard, 2)
# Probar diferentes estrategias
cart.show_receipt()
cart.set_pricing_strategy(LoyaltyPricing())
cart.show_receipt()
cart.set_pricing_strategy(BulkPricing())
cart.show_receipt()
# Usar combo strategy para mejor precio
combo = ComboStrategy([
LoyaltyPricing(),
BulkPricing(),
SeasonalPricing()
])
cart.set_pricing_strategy(combo)
cart.show_receipt() Cuándo usar Strategy:
- Múltiples algoritmos relacionados (sorting, compression, encryption)
- Eliminar condicionales complejos (if/else chains)
- Configuración en runtime (usuarios eligen comportamiento)
- A/B testing de diferentes implementaciones
Receta 2.5: Decorator - Extender funcionalidad dinámicamente
¿Qué es? Patrón que permite agregar comportamiento adicional a objetos de forma dinámica sin modificar su estructura original.
Caso de uso: Logging y métricas para funciones
import time
import functools
from typing import Callable, Any
import json
def timer(func: Callable) -> Callable:
"""Medir tiempo de ejecución"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
print(f"⏱️ {func.__name__} took {duration:.4f}s")
return result
return wrapper
def logger(func: Callable) -> Callable:
"""Registrar llamadas a función"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"📝 Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"✓ {func.__name__} returned {result!r}")
return result
return wrapper
def cache(func: Callable) -> Callable:
"""Cachear resultados de función"""
cache_dict = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Crear key del cache
key = str(args) + str(kwargs)
if key in cache_dict:
print(f"💾 Cache HIT for {func.__name__}")
return cache_dict[key]
print(f"🔄 Cache MISS for {func.__name__}, computing...")
result = func(*args, **kwargs)
cache_dict[key] = result
return result
wrapper.cache_clear = lambda: cache_dict.clear()
return wrapper
def retry(max_attempts=3, delay=1):
"""Reintentar función en caso de error"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
print(f"❌ Failed after {max_attempts} attempts")
raise
print(f"⚠️ Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
time.sleep(delay)
return wrapper
return decorator
def validate_args(**type_checks):
"""Validar tipos de argumentos"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Validar kwargs
for arg_name, expected_type in type_checks.items():
if arg_name in kwargs:
value = kwargs[arg_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{func.__name__}: '{arg_name}' debe ser {expected_type.__name__}, "
f"recibido {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
# Ejemplo: Combinar múltiples decorators
@timer
@logger
@cache
def fibonacci(n: int) -> int:
"""Calcular fibonacci con decorators"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@timer
@retry(max_attempts=3, delay=0.5)
def unreliable_api_call(fail_count=2):
"""Simular llamada API que falla aleatoriamente"""
import random
if random.random() < (fail_count / 3):
raise ConnectionError("API timeout")
return {"status": "success", "data": [1, 2, 3]}
@validate_args(amount=float, currency=str)
def process_payment(amount, currency, user_id=None):
"""Procesar pago con validación de tipos"""
print(f"💳 Processing ${amount} {currency} for user {user_id}")
return {"status": "completed"}
# Demostración
print("=== Fibonacci con cache ===")
fibonacci(5)
fibonacci(5) # Segunda llamada usa cache
fibonacci(6) # Reutiliza cálculos previos
print("
=== API con retry ===")
try:
result = unreliable_api_call()
print(f"Result: {result}")
except Exception as e:
print(f"Final error: {e}")
print("
=== Validación de argumentos ===")
process_payment(amount=99.99, currency="USD", user_id=12345)
try:
process_payment(amount="99.99", currency="USD") # Error: amount debe ser float
except TypeError as e:
print(f"Error: {e}") Decorator como clase:
class RateLimiter:
"""Limitar tasa de llamadas a función"""
def __init__(self, max_calls: int, time_window: float):
self.max_calls = max_calls
self.time_window = time_window
self.calls = []
def __call__(self, func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Limpiar llamadas antiguas
self.calls = [call_time for call_time in self.calls
if now - call_time < self.time_window]
if len(self.calls) >= self.max_calls:
raise Exception(
f"Rate limit exceeded: {self.max_calls} calls per {self.time_window}s"
)
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
@RateLimiter(max_calls=3, time_window=5.0)
def api_endpoint(request_id):
print(f"✓ Processing request {request_id}")
return {"status": "ok"}
# Probar rate limiter
for i in range(5):
try:
api_endpoint(i)
time.sleep(1)
except Exception as e:
print(f"❌ Request {i}: {e}") Aplicaciones reales:
- Python:
@property,@staticmethod,@classmethod - Web frameworks:
@login_required,@cache_page - Testing:
@mock,@patch - Async:
@async_to_sync