🧑‍💻 FASE 1: Fundamentos Reforzados - Patrones de Diseño

Recetas prácticas para dominar los fundamentos técnicos del desarrollo de software


📚 Tabla de Contenidos

  1. Singleton - Una única instancia
  2. Factory - Creación de objetos flexible
  3. Observer - Notificación de cambios
  4. Strategy - Algoritmos intercambiables
  5. 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:

Cuándo NO usar:


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:


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:


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: