Módulo 7: Modernizar Legacy Code

Proyecto del Módulo: Modernización de Módulo Legacy

Proyecto del Módulo: Modernización de Módulo Legacy

Descripción del proyecto

Vas a modernizar un módulo Python con al menos 5 items de tech debt. Cada cambio tiene test de regresión y commit separado. El entregable es el módulo modernizado + un change log detallado.

Este proyecto es directamente parte del Módulo 8 (Proyecto Integrador) donde la modernización es uno de los pasos de la migración completa de un proyecto legacy.


Objetivo del Proyecto

Modernizar un módulo legacy de forma incremental, preservando comportamiento con tests en cada paso.

Al completar:

  • ✅ Habrás ejecutado un tech debt scan completo
  • ✅ Habrás priorizado items con la matriz impacto/riesgo
  • ✅ Habrás modernizado al menos 5 items de tech debt
  • ✅ Cada modernización tiene tests de regresión
  • ✅ Cada modernización tiene commit separado
  • ✅ Change log documenta cada cambio

Especificaciones Técnicas

Módulo a Modernizar

Crea este módulo legacy con tech debt intencional:

# src/legacy_module.py
import os
import sys
import json
from datetime import datetime, timedelta

CACHE = {}
API_URL = "https://old-api.example.com"

def get_data(type, id):
    """Get data from cache or API."""
    key = "%s_%s" % (type, id)
    if CACHE.get(key):
        return CACHE[key]
    
    try:
        # Simulate API call
        if type == "user":
            data = {"id": id, "name": "User %d" % id, "type": type}
        elif type == "product":
            data = {"id": id, "name": "Product %d" % id, "price": id * 10.0}
        else:
            data = None
        
        if data:
            CACHE[key] = data
        return data
    except:
        return None

def process_items(items):
    results = []
    for i in range(len(items)):
        item = items[i]
        if item.get("active") == True:
            total = item["price"] * item["quantity"]
            tax = total * 0.16
            if item.get("region") == "EU":
                tax = total * 0.21
            elif item.get("region") == "UK":
                tax = total * 0.20
            final = total + tax
            result = {
                "name": item["name"],
                "total": final,
                "tax": tax,
                "processed_at": str(datetime.now())
            }
            results.append(result)
    return results

def save_to_file(data, filename):
    path = os.path.join(os.getcwd(), "output", filename)
    if not os.path.exists(os.path.dirname(path)):
        os.makedirs(os.path.dirname(path))
    f = open(path, "w")
    f.write(json.dumps(data, indent=2))
    f.close()
    return path

def load_from_file(filename):
    path = os.path.join(os.getcwd(), "output", filename)
    if os.path.exists(path):
        f = open(path, "r")
        data = json.loads(f.read())
        f.close()
        return data
    return None

def old_format_price(amount):
    """Deprecated: no longer used."""
    return "$%.2f" % amount

def old_validate(data):
    """This function is never called anywhere."""
    if data is None:
        return False
    if type(data) == dict:
        return len(data) > 0
    return True

class Config:
    def __init__(self, host, port, debug, timeout):
        self.host = host
        self.port = port
        self.debug = debug
        self.timeout = timeout
    
    def __repr__(self):
        return "Config(host=%s, port=%s)" % (self.host, self.port)
    
    def __eq__(self, other):
        return (self.host == other.host and self.port == other.port
                and self.debug == other.debug and self.timeout == other.timeout)

Tech Debt en Este Módulo

  1. Dead code: import sys no usado, old_format_price(), old_validate(), API_URL no referenciado
  2. %-formatting: 5+ instancias de %s y %d
  3. bare except: except: sin especificar excepción
  4. No context managers: open() sin with
  5. os.path: debería ser pathlib
  6. No type hints: ninguna función tiene type hints
  7. Magic strings/numbers: 0.16, 0.21, "EU", "UK" hardcodeados
  8. type() == X: debería ser isinstance()
  9. range(len()): debería ser iteración directa
  10. Config class: candidata a dataclass
  11. == True: comparación explícita innecesaria

Plan de Modernización (5 pasos mínimos)

Paso 1: Tests de regresión

> "Escribe tests de regresión para legacy_module.py:
   get_data(), process_items(), save_to_file(),
   load_from_file(), y Config. Ejecuta y confirma green."

Paso 2: Dead code removal

Eliminar: import sys, old_format_price(), old_validate(), API_URL.

Paso 3: Syntax modernization

%-formatting → f-strings, type() == X → isinstance(), range(len()) → iteración directa, == True → booleano directo.

Paso 4: Pattern modernization

open() → context managers, os.path → pathlib, bare except → specific exceptions, magic numbers → constants/enum.

Paso 5: Type hints + dataclass

Agregar type hints a funciones públicas. Config → dataclass.


Entregable

CHANGE_LOG.md

# Change Log: legacy_module.py Modernization

## Step 1: Tests de Regresión
- 12 tests escritos cubriendo todas las funciones
- All green ✅

## Step 2: Dead Code Removal
- Removed: import sys, old_format_price(), old_validate(), API_URL
- Tests: ✅ (12/12 pass)
- Commit: "remove dead code from legacy_module"

## Step 3: Syntax Modernization
- Changed: 5x %-formatting → f-strings
- Changed: 2x type() → isinstance()
- Changed: 1x range(len()) → direct iteration
- Changed: 1x == True → boolean
- Tests: ✅ (12/12 pass)
- Commit: "modernize syntax in legacy_module"

## Step 4: Pattern Modernization
- Changed: 2x open() → with statement (pathlib)
- Changed: 1x bare except → except Exception
- Changed: os.path → pathlib throughout
- Added: TAX_RATES constant dict for magic numbers
- Tests: ✅ (12/12 pass)
- Commit: "modernize patterns in legacy_module"

## Step 5: Type Hints + Dataclass
- Added: type hints to all public functions
- Changed: Config class → @dataclass
- Tests: ✅ (12/12 pass)
- Commit: "add type hints and convert Config to dataclass"

## Metrics
| Metric | Before | After |
|--------|--------|-------|
| Lines | 95 | 78 |
| Dead code items | 4 | 0 |
| %-formatting | 5 | 0 |
| Type hints | 0% | 100% public |
| Context managers | 0 | 2 |
| Tech debt items | 11 | 0 |

Criterios de Éxito

  • ✅ Tech debt scan documenta al menos 5 items
  • ✅ Al menos 5 modernizaciones ejecutadas
  • ✅ Tests de regresión escritos ANTES de cualquier cambio
  • ✅ Tests pasan después de CADA paso
  • ✅ 1 commit por tipo de modernización
  • ✅ CHANGE_LOG.md documenta cada cambio con métricas

Rúbrica de Evaluación (100 puntos)

Tech Debt Scan (20 puntos)

  • (10 pts) Inventario completo con tipos y severidades
  • (10 pts) Priorización con justificación

Modernización (40 puntos)

  • (8 pts) Dead code eliminado
  • (8 pts) Syntax modernizada (f-strings, isinstance, etc.)
  • (8 pts) Patterns modernizados (context managers, pathlib)
  • (8 pts) Type hints agregados
  • (8 pts) Al menos 1 modernización adicional (dataclass, enums)

Testing (25 puntos)

  • (10 pts) Tests escritos ANTES de cambios
  • (10 pts) Tests pasan después de CADA paso
  • (5 pts) Coverage de funciones principales

Documentación (15 puntos)

  • (10 pts) CHANGE_LOG con detalle por paso
  • (5 pts) Métricas before/after

Extra Credit (+10 puntos)

  • (+3 pts) 7+ modernizaciones ejecutadas
  • (+3 pts) Git history limpio con mensajes descriptivos
  • (+2 pts) Comparison de legibilidad before/after
  • (+2 pts) Todas las funciones tienen docstrings

Errores Comunes

  1. Modernizar sin tests primero — sin safety net, no sabes si rompiste algo
  2. Mezclar tipos de modernización — f-strings + type hints en el mismo commit
  3. Eliminar "dead code" que en realidad se usa — verifica con grep antes de eliminar
  4. Over-modernizar — no todo necesita ser dataclass o usar walrus operator
  5. No documentar — el CHANGE_LOG es parte del entregable

Recursos para el Proyecto

  1. pyupgrade - Para verificar tu modernización
  2. vulture - Para verificar dead code
  3. mypy - Para verificar type hints
  4. pytest - Para tests de regresión
  5. ruff - Linter ultra-rápido para Python
  6. pathlib documentation - Para reemplazar os.path

¿Qué Hacer si Te Atoras?

Si no sabes por dónde empezar:
  → Ejecuta el tech debt scan PRIMERO (cápsula 02)
  → Sin scan, modernizar es subjetivo
  → El scan te da el orden de priorización

Si los tests no pasan después del paso 2 (dead code):
  → Algo del "dead code" no era dead
  → Revierte el último cambio
  → Verifica con grep que la función realmente no se usa
  → Si se usa dinámicamente, márcala como "candidate" no "dead"

Si te tienta hacer todos los cambios juntos para "ahorrar tiempo":
  → Lee la cápsula 04 otra vez
  → El costo total de big bang es 30-50% MAYOR, no menor
  → Resiste la tentación

Si Claude Code "termina" la modernización en un solo paso:
  → Detente. Revierte. Pídele explícitamente: "solo el paso N"
  → No aceptes el primer output si no respeta la incrementalidad
  → Esto es ejercer trust calibration (guía #1 M5)

Si CHANGE_LOG.md se siente como burocracia:
  → Es el entregable que demuestra el proceso
  → Sin él, no puedes probar que fue incremental
  → 5 minutos por paso de documentación = 25 min total para 5 pasos

Evidencia de Éxito (Auto-Verificación)

Antes de declarar el proyecto completo, valida que cumples estos checkpoints:

Tech Debt Scan

  • ✅ Inventario tiene al menos 5 items con tipo, severidad, y fix sugerido
  • ✅ Priorización justificada con la matriz impacto/riesgo (cápsula 02)
  • ✅ Items de seguridad identificados (si aplican) marcados como crítico/alto

Tests

  • ✅ Los tests de regresión existen ANTES del primer cambio
  • ✅ Cada paso cierra con tests verdes — no con "los voy a arreglar después"
  • ✅ La suite completa pasa al final del paso 5

Modernización

  • ✅ 5+ tipos distintos de modernización ejecutados
  • ✅ 5+ commits separados (uno por tipo)
  • ✅ Cada commit tiene mensaje descriptivo (no "WIP", "fix", "modernize")

Documentación

  • ✅ CHANGE_LOG.md tiene una sección por cada paso
  • ✅ Cada sección documenta: qué se cambió, por qué, tests resultantes
  • ✅ Tabla de métricas before/after presente y precisa

Calidad

  • ✅ El módulo modernizado pasa pyupgrade --py310-plus sin más cambios
  • ✅ El módulo modernizado pasa vulture sin reportar dead code
  • ✅ Todas las funciones públicas tienen type hints (si el proyecto los usa)

Si los 12 puntos están en su lugar, el proyecto demuestra modernización profesional. Si dudas en alguno, vuelve a la cápsula correspondiente.


Conexión con Siguiente Módulo

Este proyecto cierra el módulo de modernización. El Módulo 8: Proyecto Integrador toma todas las técnicas de la guía completa — onboarding, exploración, architecture analysis, refactoring, migración, context management, y modernización — y las aplica en una migración de punta a punta de un proyecto legacy real.

Lo que hiciste aquí (modernización de un módulo) es un paso del proyecto integrador. Allá vas a modernizar 3-5 módulos como parte de un workflow más amplio que incluye también architecture analysis, migración de framework, y handoff documentation.