Arquitectura Limpia en Aplicaciones de Consola (CLI) con Node.js
Cuando aprendemos a programar, la calculadora suele ser uno de los primeros proyectos que construimos. Sin embargo, en un entorno profesional de backend con Node.js, construir una Interfaz de Línea de Comandos (CLI) interactiva requiere manejar flujos de datos asíncronos y estructuras de control eficientes.
Este snippet no es solo una calculadora; es una demostración de Arquitectura Funcional y Programación Asíncrona aplicada a la terminal del sistema, reemplazando los antipatrones comunes por soluciones robustas y escalables.
1. El Patrón Diccionario (Adiós al Switch/Case)
El error más común al evaluar múltiples operadores aritméticos es crear un bloque switch gigante o una cadena interminable de sentencias if-else. En este código, implementamos un Diccionario de Operaciones (Hash Map) utilizando un objeto literal de JavaScript.
Cada clave del objeto es un operador (ej. +), y su valor es una función de flecha anónima que ejecuta la lógica correspondiente. Esto proporciona un tiempo de acceso de O(1) y hace que el código sea infinitamente escalable: si el día de mañana queremos agregar una operación de raíz cuadrada o potencia, solo agregamos una línea al diccionario sin alterar la lógica de ejecución principal.
2. "Promesificación" del módulo Readline
El módulo nativo readline de Node.js está diseñado históricamente con una arquitectura basada en callbacks. Preguntar múltiples cosas al usuario de forma secuencial generaría el temido Callback Hell (Código en forma de pirámide).
Para solucionar esto y mantener un código limpio, creé una función de utilidad llamada question que envuelve el método rl.question nativo dentro de una Promesa. Esto nos permite utilizar la sintaxis moderna de async/await en nuestra función principal runCalculator(), haciendo que el código asíncrono se lea de forma secuencial y estructurada, exactamente igual que el código síncrono.
3. Manejo de Excepciones y Robustez (Try/Catch)
Un producto de software profesional no asume que el usuario ingresará los datos correctos. El bloque try/catch/finally es vital aquí:
- Validación de Tipos: Forzamos la conversión a flotantes (
parseFloat) y verificamos inmediatamente conisNaN()si el usuario introdujo letras en lugar de números, lanzando un error personalizado antes de intentar cualquier cálculo. - Manejo de Casos Límite: El diccionario contiene un operador ternario explícito para evitar el colapso del sistema matemático por una división por cero.
- Liberación de Memoria: El bloque
finallygarantiza que, sin importar si la ejecución fue un éxito o arrojó un error, el hilo dereadlinese cierre correctamente conrl.close(), evitando fugas de memoria (memory leaks) en el proceso de Node.js.
Pruébalo en la Terminal
Al ejecutar este código en un entorno interactivo (como el Playground adjunto basado en Worker Threads), notarás cómo el Event Loop de Node.js se pausa educadamente, esperando tu interacción gracias a la implementación de Promesas. Es la base perfecta para construir CLI tools más complejas como generadores de código o asistentes de configuración de servidores.