Saltar al contenido principal

Calidad y Seguridad

16. Reglas de Optimización del Código

Regla Principal

Regla: La optimización del código debe enfocarse en mejoras algorítmicas y estructurales significativas, no en micro-optimizaciones prematuras. Antes de optimizar, se debe identificar cuellos de botella reales mediante profiling.

  1. Algoritmos Eficientes: Elegir algoritmos con complejidad temporal y espacial adecuada para el problema.
  2. Evitar Trabajo Innecesario: No realizar cálculos o procesamientos cuyos resultados no se utilizan.
  3. Memoización/Caching: Aplicar memoización a funciones puras costosas que se llaman repetidamente con los mismos argumentos. Usar caching para resultados de operaciones I/O costosas.
  4. Profiling: Utilizar herramientas de profiling (navegador, Node.js) para identificar las partes del código que consumen más tiempo/recursos antes de intentar optimizar.

Contexto y Justificación

La optimización prematura es a menudo innecesaria, puede oscurecer el código y consumir tiempo de desarrollo. Enfocarse en buenos algoritmos y estructuras desde el principio es más efectivo. El profiling asegura que los esfuerzos de optimización se dirijan a los problemas reales.

Ejemplos y Contraejemplos

  • Correcto (Memoización):
    import _ from 'lodash'; // O una librería de memoización

    function calculoCostoso(n: number): number {
    console.log('Calculando...', n);
    // Simula operación pesada
    let result = 0;
    for(let i = 0; i < n * 1e6; i++) { result += Math.random(); }
    return result;
    }

    const calculoMemoizado = _.memoize(calculoCostoso);

    console.log(calculoMemoizado(5)); // Calcula y muestra 'Calculando... 5'
    console.log(calculoMemoizado(5)); // Devuelve resultado cacheado, no recalcula
    console.log(calculoMemoizado(10)); // Calcula y muestra 'Calculando... 10'
    console.log(calculoMemoizado(10)); // Devuelve resultado cacheado
  • Incorrecto (Micro-optimización vs Claridad):
    // INCORRECTO: Sacrificar legibilidad por micro-optimización insignificante
    // let x = (a + b) * c; // Legible
    // let x = a * c + b * c; // Posiblemente micro-optimizado pero menos claro

    // INCORRECTO: Optimizar sin profiling
    // Optimizar una función que raramente se llama o consume poco tiempo.

Cuándo Aplicar

Profiling cuando se detecta lentitud. Memoización/caching para operaciones costosas y repetitivas. Elección de algoritmos siempre.

Cuándo Evitar o Flexibilizar

Evitar micro-optimizaciones prematuras que dificulten la lectura. No optimizar sin medir primero.

17. Reglas de Manejo de Errores y Excepciones

Regla Principal

Regla:

  1. Manejo Explícito: Todo error previsible (ej. fallo de red, entrada inválida, recurso no encontrado) debe ser manejado explícitamente usando try/catch para operaciones síncronas y asíncronas (async/await), o .catch() en Promesas si no se usa async/await.
  2. Errores Personalizados: Crear clases de error personalizadas que hereden de Error para representar errores específicos del dominio o la aplicación. Esto facilita la identificación y manejo diferenciado de errores.
  3. No Suprimir Errores: Evitar bloques catch vacíos o que simplemente loggean el error sin tomar acción (relanzar, retornar un estado de error, etc.). Los errores deben manejarse o propagarse.
  4. Lanzar Excepciones Apropiadas: Lanzar (throw) excepciones cuando una función no puede cumplir su contrato debido a un error irrecuperable en su contexto. Usar errores estándar (TypeError, RangeError) o personalizados.

Contexto y Justificación

Un manejo de errores robusto es crucial para la estabilidad y fiabilidad de la aplicación. El manejo explícito evita caídas inesperadas. Errores personalizados permiten un control de flujo más granular. Suprimir errores dificulta la depuración. Lanzar excepciones adecuadas comunica claramente los problemas.

Ejemplos y Contraejemplos

  • Correcto (Try/Catch, Error Personalizado):
    class ValidationError extends Error {
    constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
    }
    }

    function procesarEntrada(input: any) {
    try {
    if (typeof input !== 'string' || input.length === 0) {
    throw new ValidationError('Entrada inválida: debe ser un string no vacío.');
    }
    // ... procesamiento
    console.log('Procesado:', input);
    } catch (error) {
    if (error instanceof ValidationError) {
    console.error('Error de validación:', error.message);
    // Manejo específico para errores de validación
    } else {
    console.error('Error inesperado:', error);
    // Manejo genérico o relanzar
    // throw error;
    }
    }
    }
    procesarEntrada(''); // Error de validación: Entrada inválida...
    procesarEntrada(123); // Error de validación: Entrada inválida...
  • Incorrecto (Catch vacío, Sin manejo):
    // INCORRECTO: Catch vacío (suprime el error)
    try {
    operacionPeligrosa();
    } catch (error) { }

    // INCORRECTO: Sin manejo explícito (puede crashear)
    function dividir(a: number, b: number) {
    if (b === 0) {
    throw new Error('División por cero'); // Se lanza pero ¿quién lo captura?
    }
    return a / b;
    }
    // const resultado = dividir(10, 0); // Crasheará si no se envuelve en try/catch

Cuándo Aplicar

Siempre que se realicen operaciones que puedan fallar (I/O, parsing, validaciones, llamadas a API, etc.).

Cuándo Evitar o Flexibilizar

No se debe evitar el manejo de errores. La estrategia específica (loggear, retornar valor/estado de error, relanzar) dependerá del contexto y la arquitectura.

18. Reglas sobre Patrones de Diseño Comunes

Regla Principal

Regla: La IA debe ser capaz de identificar y aplicar patrones de diseño creacionales, estructurales y de comportamiento comunes cuando sean apropiados para resolver problemas específicos de diseño, mejorar la reutilización, la flexibilidad o la mantenibilidad. El patrón aplicado debe ser justificado.

  1. Patrones Creacionales (Ej: Singleton, Factory Method, Abstract Factory, Builder): Usar para abstraer o controlar la creación de objetos.
  2. Patrones Estructurales (Ej: Adapter, Decorator, Facade, Proxy, Composite): Usar para organizar clases y objetos en estructuras más grandes y flexibles.
  3. Patrones de Comportamiento (Ej: Observer, Strategy, Command, Template Method, State, Iterator): Usar para gestionar algoritmos, relaciones y responsabilidades entre objetos. Se debe evitar la sobreingeniería aplicando patrones innecesariamente complejos a problemas simples.

Contexto y Justificación

Los patrones de diseño son soluciones probadas y documentadas a problemas recurrentes en el diseño de software. Su uso adecuado puede mejorar significativamente la calidad del código. Sin embargo, aplicarlos incorrectamente o donde no se necesitan añade complejidad innecesaria.

Ejemplos y Contraejemplos

  • Correcto (Ej: Strategy para diferentes métodos de pago):
    interface MetodoPagoStrategy {
    pagar(monto: number): void;
    }

    class TarjetaCredito implements MetodoPagoStrategy {
    pagar(monto: number) { console.log(`Pagado ${monto} con Tarjeta`); }
    }
    class PayPal implements MetodoPagoStrategy {
    pagar(monto: number) { console.log(`Pagado ${monto} con PayPal`); }
    }

    class CarritoCompra {
    private metodoPago: MetodoPagoStrategy;
    setMetodoPago(metodo: MetodoPagoStrategy) { this.metodoPago = metodo; }
    finalizarCompra(monto: number) {
    if (!this.metodoPago) throw new Error('Método de pago no seleccionado');
    this.metodoPago.pagar(monto);
    }
    }

    const carrito = new CarritoCompra();
    carrito.setMetodoPago(new PayPal());
    carrito.finalizarCompra(100);
    Justificación: El patrón Strategy permite cambiar fácilmente el algoritmo (método de pago) sin modificar CarritoCompra.
  • Incorrecto (Ej: Singleton innecesario):
    // INCORRECTO: Usar Singleton para una clase de utilidad simple sin estado
    class MathUtils {
    private static instance: MathUtils;
    private constructor() {}
    static getInstance(): MathUtils { /*...*/ return this.instance; }
    sumar(a: number, b: number) { return a + b; }
    }
    // MathUtils.getInstance().sumar(1, 2); // Verboso e innecesario

    // Alternativa Correcta: Métodos estáticos o exportación de funciones
    class MathUtilsCorrecto { static sumar(a: number, b: number) { return a + b; } }
    MathUtilsCorrecto.sumar(1, 2);
    Justificación: Aplicar Singleton a una clase sin estado añade complejidad sin beneficios reales.

Cuándo Aplicar

Cuando un problema de diseño conocido se alinea claramente con la intención de un patrón específico y su aplicación aporta beneficios tangibles (flexibilidad, desacoplamiento, reutilización).

Cuándo Evitar o Flexibilizar

Evitar aplicar patrones por moda o si la solución simple es suficiente. No forzar un patrón si no encaja naturalmente. Priorizar la simplicidad y la solución más directa al problema.

19. Reglas de Seguridad en el Código

Regla Principal

Regla: El código generado debe incorporar prácticas de seguridad fundamentales para prevenir vulnerabilidades comunes.

  1. Validación de Entradas: Validar y sanitizar rigurosamente toda entrada externa (API requests, formularios, query params) para prevenir Inyección SQL, XSS, etc. Usar DTOs con class-validator (como se vio antes).
  2. Prevención de XSS (Cross-Site Scripting) (Frontend): No insertar HTML o scripts provenientes de entradas de usuario directamente en el DOM. Usar textContent en lugar de innerHTML. Utilizar frameworks/librerías que escapen automáticamente (React, Vue, Angular).
  3. Manejo Seguro de Autenticación/Autorización: No almacenar información sensible (contraseñas, tokens) en texto plano. Usar hashing robusto (bcrypt, Argon2) para contraseñas. Implementar mecanismos seguros para gestión de sesiones/tokens (JWT, cookies seguras HttpOnly, SameSite).
  4. Dependencias Seguras: Mantener las dependencias actualizadas y usar herramientas (npm audit, yarn audit, Snyk) para identificar y corregir vulnerabilidades conocidas en paquetes de terceros.
  5. Exposición Mínima de Información: No exponer detalles internos (stack traces, mensajes de error detallados) al cliente en producción.

Contexto y Justificación

La seguridad no es una ocurrencia tardía. Integrar prácticas seguras desde el desarrollo es crucial para proteger la aplicación y los datos de los usuarios contra ataques comunes.

Ejemplos y Contraejemplos

  • Correcto (Validación, Prevención XSS):
    // Backend (NestJS DTO - ya visto)
    // Usa ValidationPipe globalmente

    // Frontend (Evitar innerHTML)
    const userInput = '<img src=x onerror=alert("XSS")>';
    const div = document.getElementById('output');
    // Correcto: Inserta como texto plano
    div.textContent = userInput;
    // Incorrecto: Vulnerable a XSS
    // div.innerHTML = userInput;
  • Incorrecto (Contraseña en texto plano, Error detallado):
    // INCORRECTO: Guardar contraseña sin hashear
    function guardarUsuario(email: string, pass: string) {
    // db.save({ email: email, password: pass }); // ¡Muy inseguro!
    }

    // INCORRECTO: Exponer error detallado en producción
    function algunaOperacion() {
    try { /* ... */ } catch (error) {
    // return { success: false, error: error.stack }; // ¡No hacer esto en producción!
    return { success: false, message: 'Ocurrió un error interno.' }; // Mejor
    }
    }

Cuándo Aplicar

Siempre. La seguridad debe ser una consideración constante en todo el ciclo de vida del desarrollo.

Cuándo Evitar o Flexibilizar

Nunca se deben flexibilizar las prácticas fundamentales de seguridad. El nivel de rigor puede variar ligeramente según el riesgo (aplicación interna vs. pública), pero los principios básicos son innegociables.

20. Reglas sobre Testing (Conceptos y Herramientas)

Regla Principal

Regla: La IA debe ser capaz de generar código testeable y, cuando se le solicite, generar pruebas unitarias, de integración y/o E2E básicas utilizando frameworks y librerías estándar del ecosistema JS/TS.

  1. Código Testeable: Escribir código modular, con dependencias claras (inyección de dependencias), funciones puras siempre que sea posible, y responsabilidades bien definidas (SOLID).
  2. Pruebas Unitarias: Enfocadas en unidades aisladas (funciones, métodos de clase). Usar frameworks como Jest, Vitest, Mocha. Mockear dependencias externas (usando jest.fn(), vi.fn(), Sinon.JS).
  3. Pruebas de Integración: Verificar la interacción entre varios componentes/módulos. Pueden requerir un entorno más complejo (ej. base de datos de prueba).
  4. Pruebas E2E (End-to-End): Simular flujos de usuario completos a través de la interfaz. Usar herramientas como Cypress, Playwright, Puppeteer.
  5. Cobertura (Coverage): Generar reportes de cobertura como guía, pero priorizar la calidad y relevancia de las pruebas sobre el porcentaje bruto.

Contexto y Justificación

El testing es esencial para asegurar la calidad, la corrección y prevenir regresiones. Escribir código testeable desde el principio facilita enormemente la creación de pruebas efectivas. Los diferentes tipos de pruebas cubren distintos niveles de la aplicación.

Ejemplos y Contraejemplos

  • Correcto (Código Testeable, Ejemplo Prueba Unitaria con Jest):
    // ---- src/sumador.ts ----
    export function sumar(a: number, b: number): number {
    return a + b;
    }

    // ---- test/sumador.test.ts ----
    import { sumar } from '../src/sumador';

    describe('sumar', () => {
    it('debe sumar dos números positivos', () => {
    expect(sumar(2, 3)).toBe(5);
    });

    it('debe sumar un número positivo y cero', () => {
    expect(sumar(5, 0)).toBe(5);
    });

    it('debe sumar dos números negativos', () => {
    expect(sumar(-1, -4)).toBe(-5);
    });
    });
    Justificación: La función sumar es pura y fácil de testear. La prueba unitaria verifica varios casos de forma aislada usando Jest.
  • Incorrecto (Código difícil de testear):
    // INCORRECTO: Acoplamiento fuerte, difícil de mockear
    function procesarUsuarioLegacy(userId: number) {
    // Dependencia directa y difícil de mockear
    const dbConnection = new DatabaseConnection('prod.db.com');
    const userData = dbConnection.query(`SELECT * FROM users WHERE id=${userId}`);
    // ... más lógica acoplada
    return userData;
    }

    // Para testear esto, se necesitaría una BD real o mocks complejos.
    Justificación: La función crea su propia dependencia (DatabaseConnection), haciendo imposible testearla sin una base de datos real o técnicas de mocking avanzadas. Inyectar la dependencia sería mucho mejor.

Cuándo Aplicar

Escribir código testeable siempre. Generar pruebas según los requisitos del proyecto y la criticidad del código.

Cuándo Evitar o Flexibilizar

El tipo y cantidad de pruebas dependen del contexto. No todas las partes del código requieren el mismo nivel de testing. Evitar pruebas frágiles (brittle tests) que se rompen con cambios mínimos de implementación.