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 entilesetTextureCachecreateMaterialsFromSelectedTiles()— 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
- Refactorización incremental: Extraer handlers uno por uno permite verificación manual en cada paso.
- El nesting no es siempre anidamiento real: Las cadenas de
if constexprson ramas mutuamente excluyentes al mismo nivel, no nesting real. - Los helpers eliminan duplicación: Dos funciones compartidas eliminaron ~50 líneas de código duplicado.
- 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.