El Problema: Una God Function de 373 Líneas

La función EditorActions::executeEditorActions() era el corazón del sistema de acciones del editor. Con 373 líneas, complejidad cognitiva de 332, y un Maintainability Index de 22.9, era un claro ejemplo de god function — una función que hace demasiado y es imposible de mantener.

Métricas Antes de la Refactorización

Métrica Valor
Líneas 373
Complejidad cognitiva 332
Complejidad ciclomática 58
Nesting máximo 12 niveles
Maintainability Index 22.9
Halstead effort 887,606

La Solución: Descomposición en Handlers

La refactorización se realizó en dos fases, siguiendo un enfoque incremental y seguro.

Fase 1: Extracción de Handlers

Descompusimos la función monolítica en un dispatcher fino + handlers individuales. Cada acción del editor ahora tiene su propia función:

// Antes: 373 líneas en un solo lambda
std::visit([&](auto&& action) {
    using T = std::decay_t<decltype(action)>;
    if constexpr (std::is_same_v<T, Quit>) {
        // 20 líneas de código
    } else if constexpr (std::is_same_v<T, SaveProject>) {
        // 30 líneas de código
    }
    // ... 11 más branches
}, editorAction);

// Después: Dispatcher de 27 líneas
std::visit([&](auto&& action) {
    using T = std::decay_t<decltype(action)>;
    if constexpr (std::is_same_v<T, Quit>) {
        handleQuit(ctx, action);
    } else if constexpr (std::is_same_v<T, SaveProject>) {
        handleSaveProject(ctx, action);
    }
    // ... delega a handlers
}, editorAction);

Handlers Extraídos

Handler Responsabilidad
handleQuit Cierra la ventana
handleSaveProject Guarda tilesets a disco
handleLoadProject Carga proyecto desde JSON
handleLoadLevel Carga nivel desde archivo
handleCreateLevel Genera nivel desde PNG
handleSaveLevel Guarda nivel a archivo
handleCreateMaterial Crea material desde PNG
handleCreateTileset Genera tileset desde PNG
handleUpdateTileset Actualiza tileset existente
handleLoadTilesets Carga todos los tilesets
handleCreateTriggerOverlay Crea overlay de trigger
handleDeleteTriggerOverlay Elimina overlay de trigger
handleUpdateTriggerOverlay Actualiza overlay de trigger

Fase 2: Eliminación de Duplicación

Extrajimos dos helpers compartidos para eliminar código duplicado en múltiples handlers:

  • loadTilesetGPUTextures() — carga texturas PNG a GPU, cachea en tilesetTextureCache
  • createMaterialsFromSelectedTiles() — genera materiales desde tiles seleccionados

Impacto por handler:

Handler Nesting antes Nesting después Cambio
handleLoadProject 4 2 -2 niveles
handleCreateMaterial 3 1 -2 niveles
handleCreateTileset 5 3 -2 niveles
handleLoadTilesets 4 2 -2 niveles

~50 líneas de código duplicado reemplazadas por llamadas a helpers.

Resultados: Métricas Finales

Métrica Antes Después Mejora
Líneas (dispatcher) 373 27 -93%
Complejidad cognitiva 332 76 -77%
Complejidad ciclomática 58 13 -78%
Maintainability Index 22.9 46.1 +101%
Halstead effort 887,606 24,735 -97%

Decisiones de Diseño

D1: Funciones libres (no clases handler)

Elegimos funciones libres en lugar de clases handler con interfaz IActionHandler. Las funciones libres son más simples, fáciles de encontrar, y fáciles de depurar. La firma auto handleLoadLevel(EditorContext& ctx, const LoadLevel& action) -> void es auto-documentada.

D2: Mantener std::visit para dispatch

El problema no era el mecanismo de dispatch (std::visit), sino el tamaño del lambda. Mantener std::visit pero delegando a funciones de una línea resuelve el problema real.

D3: Centralizar validación de dependencias

Creamos validateDependencies() para eliminar los bloques duplicados de null checks en cada handler. Fuente única de verdad para mensajes de error.

Lecciones Aprendidas

  1. Refactorización incremental: Extraer handlers uno por uno permite verificación manual en cada paso.
  2. El nesting no es siempre anidamiento real: Las cadenas de if constexpr son ramas mutuamente excluyentes al mismo nivel, no nesting real.
  3. Los helpers eliminan duplicación: Dos funciones compartidas eliminaron ~50 líneas de código duplicado.
  4. Las métricas guían la refactorización: Complejidad cognitiva y Maintainability Index son indicadores claros de cuándo refactorizar.

Conclusión — Día 1

Esta refactorización demuestra cómo descomponer una god function en componentes manejables puede mejorar dramáticamente la mantenibilidad del código. Con una reducción del 93% en líneas y una mejora del 101% en el Maintainability Index, el código ahora es más fácil de entender, mantener y extender.

El enfoque incremental — extraer handlers uno por uno con verificación manual — es clave para refactorizaciones seguras de código crítico.


Día 2: 2026-06-08

3. TilesetGenerationDialog — Eliminación por herencia

Problema: TilesetGenerationDialog y TilePreviewDialog eran clases casi idénticas con código duplicado masivo. Ambas implementaban lógica de UI para generar y previsualizar tilesets, pero sin una base compartida.

Solución: Eliminar TilesetGenerationDialog y extraer toda la lógica compartida a TilesetDialogBase. TilePreviewDialog y el nuevo flujo de generación heredan de esta clase base.

Commits:

  • febecea — TilesetGenerationDialog hereda de TilesetDialogBase (5260 líneas eliminadas, 3534 creadas)
  • 8fd2acc — Simplificar TilesetDialogBase y TilePreviewDialog (789 líneas eliminadas)

Resultado: Archivo eliminado completamente (TilesetGenerationDialog.cpp/hpp), lógica unificada en TilesetDialogBase.


4. TilesetDialogBase — God Class cleanup

Archivo: src/Editor/Data/TilesetDialogBase.cpp (2490 → 2065 LOC, -425 LOC)

Después de absorber la lógica de TilesetGenerationDialog, TilesetDialogBase se convirtió en una God Class de 2490 líneas. La siguiente ronda de refactorización la limpió en 5 pasos.

4.1 Extract renderEditPreviewingPngState

Problema: renderView era 273 LOC con un case PreviewingPng de 170 líneas inline, anidada 3 niveles profundo.

Solución: Extraer el body del case a un método dedicado.

// ANTES: 170 líneas inline en el switch
case DialogState::PreviewingPng:
    if (isInEditMode()) {
        // 170 líneas de UI inline...
    }

// DESPUÉS: delegación limpia
case DialogState::PreviewingPng:
    if (isInEditMode()) {
        renderEditPreviewingPngState(result);
    } else {
        renderViewerState(tileSize);
    }
    break;

Resultado: renderView de 273 → 80 LOC. El nuevo método tiene 160 LOC con lógica de edit mode claramente separada.

4.2 Split renderGridPreview GPU/CPU

Problema: renderGridPreview era 289 LOC con 3 rendering paths mezclados en un double for loop.

Solución: Extraer GPU y CPU a métodos dedicados.

// DESPUÉS: delegación por path
if (pngPreviewTexture_ && descriptorManager_.getDescriptorSet()) {
    renderGridTileGPU(row, col, cellPos, cellDisplaySize, selectedTileSize);
} else {
    renderGridTileCPU(row, col, cellPos, cellPosEnd, cellDisplaySize, selectedTileSize, tileData);
}
Método LOC Responsabilidad
renderGridPreview 100 Orchestrator: setup, loop, overlay, stats
renderGridTileGPU 37 UV calc + ImGui::Image
renderGridTileCPU 55 Pixel-by-pixel fallback

4.3 Remove dead code: renderViewState y renderPreviewState

Problema: Ambos métodos declarados en el header pero nunca llamados. Eran abandono mid-implementation.

Verificación: grep confirmó que solo existían en su propia declaración e implementación.

Resultado: Eliminación completa (46 + 133 = 179 LOC eliminadas).

4.4 Merge generate/update button sections

Problema: renderGenerateButtonSection (53 LOC) y renderUpdateButtonSection (53 LOC) eran idénticos excepto por el label del botón.

Solución: Un solo método parametrizado.

renderActionButtonSection("Generate Tileset", "Select PNG file...");
renderActionButtonSection("Update Tileset", "Select PNG file...");

Resultado: 107 LOC → 43 LOC (-64 LOC, -60%).

4.5 Extract PngDescriptorManager

Problema: 3 métodos Vulkan de 151 LOC en TilesetDialogBase que solo accedían a su propio state.

Solución: Nueva clase en archivos propios.

Archivo LOC
PngDescriptorManager.hpp 53
PngDescriptorManager.cpp 140
TilesetDialogBase (eliminado) -151

Ventaja: El destructor maneja su propia limpieza, simplificando TilesetDialogBase a = default.

5. GUIMenus — renderTriggerTypeCombo

Archivo: src/Editor/RenderUI/GUIMenus.cpp

Problema: renderCreateOverlayDialog (120 LOC) y renderEditOverlayDialog (118 LOC) tenían 35 líneas idénticas de combo rendering para seleccionar el tipo de trigger.

Solución: Helper compartido que toma referencia al índice y un suffix para los ImGui IDs.

renderTriggerTypeCombo(overlayTriggerTypeIndex_, "##overlay");
renderTriggerTypeCombo(overlayEditTriggerTypeIndex_, "##overlay_edit");

Resultado: Ambos dialogs de ~120 → 62 LOC. Combo helper de 41 LOC.

Resumen: Editor (2 días)

Archivo LOC antes LOC después Cambio
editorActions.cpp ~500 ~350 -150
TilesetGenerationDialog 2218 0 -2218
TilesetDialogBase.cpp 2490 2065 -425
PngDescriptorManager (nuevos) 193 +193
Neto editor ~2600 LOC

Patrones aplicados (Día 2)

  • Inheritance consolidation: TilesetGenerationDialog → TilesetDialogBase
  • Method extraction: renderEditPreviewingPngState, renderGridTileGPU/CPU
  • Parameter merging: renderActionButtonSection (label + tooltip)
  • Shared helper: renderTriggerTypeCombo
  • Dead code removal: renderViewState, renderPreviewState
  • Class extraction: PngDescriptorManager (Vulkan lifecycle)

Build verification

Todos los cambios del Día 2 pasaron build sin errores nuevos.