learning-front

Nivel 4 · Tooling profesional: el entorno de un proyecto serio

Clean code: SOLID, KISS, DRY y patrones

Principios para escribir código que tu yo del futuro agradecerá: nombres reveladores, funciones de una sola responsabilidad, DRY sin abusar, SOLID en la práctica y los tres patrones de diseño que de verdad aparecen en el día a día (Factory, Singleton, Adapter).

Por qué importa#

El código se escribe una vez y se lee cien. Cada vez que abres un fichero para corregir un bug, añadir una función o entender qué hacía tu compañero, estás leyendo. Por eso el objetivo principal al escribir código no es que la máquina lo entienda (eso ya lo consigue cualquier código que compila) sino que el siguiente programador —que muy probablemente serás tú dentro de seis meses— lo entienda sin tener que descifrar.

Los principios de este capítulo no son reglas de estilo arbitrarias. Son respuestas a problemas reales que aparecen cuando el código crece: funciones que nadie se atreve a tocar porque hacen demasiado, números sin nombre cuyo significado ya nadie recuerda, clases que hay que abrir cada vez que llega un nuevo requisito.

Nombres que revelan la intención#

El nombre de una variable, función o clase es la primera documentación que lee quien mantiene el código. Un nombre opaco obliga a leer la implementación para entender el propósito.

javascript
// Antes: ¿qué es 'w'? ¿qué hace 'd'?
var w = h.victorias / h.partidas;
function d(lista, r) { /* ... */ }
javascript
// Después: el nombre ya lo dice todo.
// proporción de victorias sobre partidas
const winrate = heroe.victorias / heroe.partidas;
// filtra y ordena por rol
function heroesFiablesPorRol(lista, rol) { /* ... */ }

Reglas prácticas:

  • Variables: sustantivos que describen el contenido (winrate, heroesFiltrados, maxPartidas).
  • Funciones: verbos o preguntas que describen la acción (calcularWinrate, esFiable, heroesPorRol).
  • Booleanos: preguntas con respuesta sí/no (esFiable, tienePartidas, estaDisponible).
  • Constantes: mayúsculas con guion bajo para los valores que no cambian (MINIMO_PARTIDAS_FIABLE).

Evita abreviaturas (h, r, tmp) salvo en contextos estándar universales como i en un bucle for.

Funciones: una cosa, pequeña, sin sorpresas#

Una función que hace varias cosas a la vez tiene varias razones para cambiar, varios bugs posibles y varios contextos mentales que el lector debe cargar a la vez.

javascript
// Antes: filtra, calcula el winrate, asigna etiqueta Y ordena. Cuatro cosas.
function hacerCosas(lista, r) {
  var res = [];
  for (var i = 0; i < lista.length; i++) {
    if (lista[i].rol === r) {
      if (lista[i].partidas >= 40) {
        // calcula winrate
        var w = lista[i].victorias / lista[i].partidas;
        var etiqueta = '';
        // asigna etiqueta
        if (w >= 0.6) { etiqueta = 'estrella'; }
        else if (w >= 0.5) { etiqueta = 'solido'; }
        else { etiqueta = 'flojo'; }
        res.push({ nombre: lista[i].nombre, winrate: w, etiqueta: etiqueta });
      }
    }
  }
  // ... sort a mano de 7 líneas más
  return res;
}
javascript
// Después: cada función hace una sola cosa y tiene un nombre que lo dice.
// calcula el winrate de un héroe
function winrateDe(heroe) {
  return heroe.victorias / heroe.partidas;
}
// asigna la etiqueta según el winrate
function etiquetaPorWinrate(winrate) {
  if (winrate >= 0.6) return 'estrella';
  if (winrate >= 0.5) return 'solido';
  return 'flojo';
}
// decide si el héroe tiene datos fiables
function esFiable(heroe) {
  return heroe.partidas >= 40;
}
// construye la ficha a partir del héroe crudo
function aFicha(heroe) {
  // delega en winrateDe
  const winrate = winrateDe(heroe);
  return { nombre: heroe.nombre, winrate: winrate, etiqueta: etiquetaPorWinrate(winrate) };
}
// comparador: mayor winrate primero
function porWinrateDesc(a, b) {
  return b.winrate - a.winrate;
}
// orquesta: filtra, transforma y ordena
function heroesFiablesPorRol(lista, rol) {
  return lista
    // filtra por rol y fiabilidad
    .filter(function (h) { return h.rol === rol && esFiable(h); })
    // transforma cada héroe en una ficha
    .map(aFicha)
    // ordena de mayor a menor winrate
    .sort(porWinrateDesc);
}

La versión limpia tiene más líneas pero cada una se lee en segundos. El mantenimiento es proporcional a la claridad, no al número de líneas.

DRY y el aviso del DRY prematuro#

DRY (Don’t Repeat Yourself) dice que cada pieza de conocimiento debe vivir en un solo sitio. Si la regla “una muestra es fiable con >= 40 partidas” aparece en cinco funciones, cambiarla exige encontrarlas todas. Con una constante y una función basta con tocar un lugar.

javascript
// Sin DRY: el número 40 aparece tres veces.
// Si el equipo decide subir el umbral a 50, hay que buscarlo en todo el fichero.
if (heroe.partidas >= 40) { /* ... */ }
if (lista[i].partidas >= 40) { /* ... */ }
if (h.partidas >= 40) { /* ... */ }
javascript
// Con DRY: el conocimiento vive en un solo sitio.
// aquí se cambia y punto
const MINIMO_PARTIDAS_FIABLE = 40;
function esFiable(heroe) {
  // usa la constante
  return heroe.partidas >= MINIMO_PARTIDAS_FIABLE;
}

Aviso de DRY prematuro. Aplicar DRY demasiado pronto puede ser peor que duplicar. Si ves dos fragmentos parecidos pero que cambian por motivos distintos (el umbral de fiabilidad y el umbral de visibilidad en la UI son dos conceptos diferentes aunque hoy compartan el valor 40), unirlos crea un acoplamiento falso: si uno cambia, se arrastra al otro. La regla práctica: espera a ver la tercera repetición antes de abstraer; dos pueden ser una coincidencia.

KISS y YAGNI#

KISS (Keep It Simple) dice que la solución más simple que resuelve el problema real gana. No es sinónimo de código malo: es elegir, entre varias soluciones correctas, la que menos complejidad accidental añade.

javascript
// Sobre-ingeniería: una clase con estado, configuración inyectable y eventos,
// para algo que solo necesita filtrar un array.
class HeroeFilterEngine {
  constructor(config) { this.config = config; }
  setStrategy(strategy) { this.strategy = strategy; }
  execute(lista) { /* 40 líneas */ }
}
javascript
// KISS: una función que resuelve el problema concreto.
function heroesFiablesPorRol(lista, rol) {
  return lista.filter(function (h) { return h.rol === rol && esFiable(h); })
    .map(aFicha)
    .sort(porWinrateDesc);
}

YAGNI (You Aren’t Gonna Need It) es el complemento: no construyas la infraestructura para casos hipotéticos. Si hoy solo filtras héroes por rol, no añadas soporte para filtrar por región, por temporada o por parche “por si acaso”. Cuando ese requisito llegue (si llega), lo añades. El código no construido no tiene bugs ni mantenimiento.

SOLID: los cinco principios#

SOLID es un acrónimo con cinco principios que guían el diseño de módulos y clases. No son leyes físicas: son heurísticas que ayudan a escribir código que aguanta los cambios sin romperse.

SRP — Principio de responsabilidad única#

Una función o módulo debe tener una sola razón para cambiar.

javascript
// Viola SRP: esta función calcula el winrate Y decide si es fiable Y construye la ficha.
// Tiene tres razones distintas para cambiar.
function procesarHeroe(heroe) {
  // responsabilidad 1: calcular winrate
  var winrate = heroe.victorias / heroe.partidas;
  // responsabilidad 2: decidir si es fiable
  if (heroe.partidas < 40) return null;
  // responsabilidad 3: etiquetar
  var etiqueta = winrate >= 0.6 ? 'estrella' : 'solido';
  // responsabilidad 4: construir ficha
  return { nombre: heroe.nombre, winrate: winrate, etiqueta: etiqueta };
}
javascript
// Con SRP: cada función tiene una sola responsabilidad.
// solo calcula el winrate
function winrateDe(heroe) {
  return heroe.victorias / heroe.partidas;
}
// solo decide si hay suficientes partidas
function esFiable(heroe) {
  return heroe.partidas >= MINIMO_PARTIDAS_FIABLE;
}
// solo construye la ficha
function aFicha(heroe) {
  // delega el cálculo en winrateDe
  const winrate = winrateDe(heroe);
  return { nombre: heroe.nombre, winrate: winrate, etiqueta: etiquetaPorWinrate(winrate) };
}

OCP — Principio abierto/cerrado#

El código debe estar abierto a extensión pero cerrado a modificación. Añadir comportamiento nuevo no debe exigir reescribir el comportamiento existente.

javascript
// Viola OCP: añadir 'leyenda' exige abrir este switch y modificarlo,
// con riesgo de romper los casos existentes.
function etiquetaPorWinrate(winrate) {
  if (winrate >= 0.6) return 'estrella';
  if (winrate >= 0.5) return 'solido';
  return 'flojo';
  // Para añadir 'leyenda' hay que abrir esta función y editar sus entrañas.
}
javascript
// Con OCP: una tabla de niveles. Añadir 'leyenda' es añadir una fila.
// La función etiquetaPorWinrate no cambia: está cerrada a modificación.
const NIVELES = [
  // nueva fila — no tocas nada más
  { minimo: 0.75, etiqueta: 'leyenda' },
  { minimo: 0.6,  etiqueta: 'estrella' },
  { minimo: 0.5,  etiqueta: 'solido' },
  { minimo: 0,    etiqueta: 'flojo' },
];

function etiquetaPorWinrate(winrate) {
  // recorre la tabla de arriba a abajo
  for (let i = 0; i < NIVELES.length; i++) {
    // devuelve la primera que cumple
    if (winrate >= NIVELES[i].minimo) return NIVELES[i].etiqueta;
  }
  // fallback: la última fila
  return NIVELES[NIVELES.length - 1].etiqueta;
}

LSP — Principio de sustitución de Liskov#

Un subtipo debe poder sustituir a su tipo base sin que el código que ya existía deje de funcionar. LSP no dice “hereda sin sobrescribir nada”: dice que si sobrescribes algo, el contrato debe seguir cumpliéndose.

El punto clave es el código cliente preexistente: una función escrita antes de que existieran los subtipos.

javascript
// Clase base: cualquier héroe del Team Builder.
class Heroe {
  constructor(nombre, rol, partidas, victorias) {
    // nombre del héroe
    this.nombre = nombre;
    // 'dps', 'tank' o 'support'
    this.rol = rol;
    // total de partidas jugadas
    this.partidas = partidas;
    // total de victorias
    this.victorias = victorias;
  }
  // Contrato: winrate() devuelve un número entre 0 y 1.
  winrate() {
    // proporción de victorias
    return this.victorias / this.partidas;
  }
}

// Función cliente escrita ANTES de que existieran subtipos.
// Solo sabe que recibe un Heroe y que heroe.winrate() devuelve un número.
function describir(heroe) {
  // usa el contrato, no la implementación
  return heroe.nombre + ': ' + heroe.winrate();
}

// --- Subtipo que RESPETA el contrato ---

class HeroeLegendario extends Heroe {
  constructor(nombre, rol, partidas, victorias, skin) {
    // delega la inicialización base
    super(nombre, rol, partidas, victorias);
    // propiedad extra: nombre de la skin legendaria
    this.skin = skin;
  }
  // Sobrescribe winrate() pero sigue devolviendo un número: contrato respetado.
  winrate() {
    // Penalización cosmética, pero el resultado sigue siendo un número.
    // ajuste de balance interno
    return (this.victorias / this.partidas) - 0.02;
  }
}

// tipo base
const mercy = new Heroe('Mercy', 'support', 60, 39);
// subtipo
const ana   = new HeroeLegendario('Ana', 'support', 80, 52, 'Comandante');

// describir() funciona con ambos sin modificarse: LSP se cumple.
// 'Mercy: 0.65'
console.log(describir(mercy));
// 'Ana: 0.63' — resultado diferente, pero sigue siendo un número
console.log(describir(ana));

// --- Subtipo que VIOLA el contrato ---

// (Este ejemplo es prosa: muestra la violación sin ejecutarse)
javascript
// Viola LSP: winrate() lanza un error cuando no hay partidas,
// en vez de devolver un número como espera el contrato.
class HeroeSinPartidas extends Heroe {
  constructor(nombre, rol) {
    // sin partidas ni victorias
    super(nombre, rol, 0, 0);
  }
  winrate() {
    // VIOLA el contrato: la base devuelve un número
    throw new Error('No hay partidas registradas');
  }
}

var novato = new HeroeSinPartidas('Novato', 'dps');

// describir(novato) se rompe aunque describir() no ha cambiado.
// El cliente asumía que cualquier Heroe tiene un winrate() que devuelve un número.
// lanza Error — LSP violado
describir(novato);

La lección: LSP no prohíbe sobrescribir métodos. Prohíbe cambiar el contrato que el código cliente ya asume. Si winrate() promete devolver un número, todos los subtipos deben cumplir esa promesa, sin excepciones.

ISP — Principio de segregación de interfaces#

Las interfaces (o en JS, los objetos que pasas a una función) deben ser pequeñas y específicas. No obligues a nadie a depender de propiedades que no usa.

javascript
// Viola ISP: la función recibe el héroe completo pero solo usa 'victorias' y 'partidas'.
// El llamador tiene que proporcionar un objeto con todos los campos aunque no hagan falta.
function winrateDe(heroe) {
  // solo usa dos campos del héroe
  return heroe.victorias / heroe.partidas;
  // heroe.nombre, heroe.rol, heroe.skin... ignorados
}
javascript
// Con ISP: la función declara exactamente lo que necesita.
// Funciona con cualquier objeto que tenga 'victorias' y 'partidas', no solo con Heroe.
function calcularWinrate(victorias, partidas) {
  // interfaz mínima: solo lo que se usa
  return victorias / partidas;
}

// pasas solo lo necesario
const winrate = calcularWinrate(mercy.victorias, mercy.partidas);

En JavaScript no hay interfaces formales, así que ISP se aplica aquí como “no exijas al que llama más de lo que la función realmente necesita”. La forma plena de segregar interfaces —declarar tipos de contrato separados y verificarlos en compilación— llega con TypeScript (nivel 5). Lo que ves en este ejemplo es el espíritu del principio: si una función recibe un objeto enorme y solo usa dos campos, simplifica su firma para no crear dependencias innecesarias.

DIP — Principio de inversión de dependencias#

El código de alto nivel no debe depender de detalles de bajo nivel; ambos deben depender de abstracciones.

javascript
// Viola DIP: heroesFiablesPorRol depende de la variable global 'heroes'.
// No puedes cambiar la fuente de datos sin modificar la función.
var heroes = [
  { nombre: 'Tracer', rol: 'dps', partidas: 120, victorias: 70 },
  // ...
];

// sin parámetro de lista: lee la global
function heroesFiablesPorRol(rol) {
  // acoplada a la variable global 'heroes'
  return heroes
    .filter(function (h) { return h.rol === rol && esFiable(h); })
    .map(aFicha)
    .sort(porWinrateDesc);
}
javascript
// Con DIP: la fuente de datos entra por parámetro.
// La función depende de 'cualquier lista de héroes', no de 'la variable global heroes'.
// 'lista' es la abstracción: cualquier array de héroes
function heroesFiablesPorRol(lista, rol) {
  // no importa de dónde venga la lista
  return lista
    .filter(function (h) { return h.rol === rol && esFiable(h); })
    .map(aFicha)
    .sort(porWinrateDesc);
}

// Ahora puedes llamarla con cualquier fuente:
// con la lista real
heroesFiablesPorRol(heroes, 'dps');
// con datos de test
heroesFiablesPorRol(heroesDePrueba, 'dps');
// con datos de una API
heroesFiablesPorRol(heroesDeLaAPI, 'dps');

En su forma plena, el DIP da un paso más: el código de alto nivel depende de un contrato (una interfaz) y no de una implementación concreta, de modo que puedes cambiar la implementación —de dónde salen los datos, qué base de datos hay detrás— sin tocar el código que la usa. En JavaScript ese contrato es implícito (“cualquier array de héroes”); cuando llegues a TypeScript (nivel 5) podrás declararlo de forma explícita y que el compilador lo verifique, igual que pasa con ISP.

Patrones que de verdad se usan#

Los patrones de diseño son soluciones con nombre a problemas que se repiten. No son plantillas que se copian: son vocabulario compartido (“aquí hay un Factory”, “esto es un Adapter”) que hace que las revisiones de código sean más rápidas.

Factory — crear sin acoplarte a la clase concreta#

El patrón Factory centraliza la creación de objetos. La creación de cada héroe es idéntica salvo por el campo habilidad, que depende del rol. Sin Factory, esa lógica se repite cada vez que alguien crea un héroe.

javascript
// Sin Factory: la lógica de qué habilidad tiene cada rol está repetida en cada sitio.
// Si el rol 'dps' cambia de 'movilidad' a 'daño', hay que buscar y cambiar cada uno.
const tracer = { nombre: 'Tracer', rol: 'dps', partidas: 120, victorias: 70, habilidad: 'movilidad' };
// el llamador tiene que recordar qué habilidad corresponde a cada rol
const mercy  = { nombre: 'Mercy', rol: 'support', partidas: 60, victorias: 39, habilidad: 'curación' };
javascript
// Con Factory: el llamador solo conoce crearHeroe(); la decisión de qué forma
// tiene el objeto de cada rol vive en un solo sitio.
function crearHeroe(nombre, rol, partidas, victorias) {
  // La Factory decide la forma del objeto según el rol.
  if (rol === 'dps') {
    return { nombre: nombre, rol: 'dps', partidas: partidas, victorias: victorias, habilidad: 'movilidad' };
  }
  if (rol === 'support') {
    return { nombre: nombre, rol: 'support', partidas: partidas, victorias: victorias, habilidad: 'curación' };
  }
  if (rol === 'tank') {
    return { nombre: nombre, rol: 'tank', partidas: partidas, victorias: victorias, habilidad: 'resistencia' };
  }
  // rol desconocido: objeto base
  return { nombre: nombre, rol: rol, partidas: partidas, victorias: victorias };
}

// el llamador no sabe qué objeto exacto se crea
const tracer = crearHeroe('Tracer', 'dps', 120, 70);
// la lógica de creación está en un solo lugar
const mercy  = crearHeroe('Mercy', 'support', 60, 39);

Singleton — una única instancia (con su crítica)#

El Singleton garantiza que solo existe una instancia de un objeto en toda la aplicación. El caso típico es la configuración global o el store de la aplicación.

Con módulos ESM (que ya conoces del nivel 3) esto es directo: una variable a nivel de fichero actúa como estado privado del módulo. Nadie fuera puede acceder a ella directamente; solo a través de la función que exportas.

javascript
// config-team-builder.js — Singleton con módulo ESM.
// La variable 'instancia' vive en el ámbito del módulo: es privada.
// Solo este fichero puede crearla o modificarla.
let instancia = null;

// Devuelve siempre la misma instancia de configuración.
function obtenerConfig() {
  if (instancia === null) {
    // crea el objeto de configuración solo la primera vez
    instancia = {
      // límite de héroes por rol en el equipo
      maxHeroesPorRol: 2,
      // umbrales de etiqueta
      umbrales: { estrella: 0.6, solido: 0.5 },
      // versión de la configuración
      version: '2026.1',
    };
  }
  // siempre devuelve la misma referencia
  return instancia;
}

export { obtenerConfig };
javascript
// equipo.js — cualquier fichero que importe obtenerConfig recibe la misma instancia.
import { obtenerConfig } from './config-team-builder.js';

// primera llamada: crea la instancia
const config = obtenerConfig();
// segunda llamada: devuelve la misma (instancia ya no era null)
const config2 = obtenerConfig();

// true: es la misma referencia
console.log(config === config2);

Crítica del Singleton. El Singleton es cómodo pero tiene un precio: es estado global disfrazado. Cualquier parte del código puede leer y modificar esa instancia sin que quien la llama lo sepa. Eso hace que los bugs sean difíciles de rastrear y las pruebas, difíciles de aislar. En código moderno, la alternativa es pasar la configuración por parámetro (es decir, inyectar la dependencia: dársela explícitamente a quien la necesita en vez de que la busque por su cuenta) o delegar en el framework de turno. Úsalo con moderación y documenta por qué necesitas exactamente una instancia.

Adapter — traducir interfaces sin tocar ninguna de las dos#

El Adapter traduce la interfaz de un sistema externo a la que espera tu código. Es el patrón que aparece cada vez que integras una API externa.

javascript
// Lo que devuelve la API de Blizzard: campos en inglés y en snake_case.
const respuestaBlizzard = {
  heroName: 'Mercy',
  primaryRole: 'support',
  gamesPlayed: 60,
  wins: 39,
};

// Lo que espera el resto del código del Team Builder: campos en castellano.
// { nombre, rol, partidas, victorias }

// Adapter: traduce el formato de Blizzard al modelo interno del Team Builder.
// Ninguno de los dos lados cambia: solo el adapter.
function adaptarHeroeBlizzard(apiData) {
  return {
    // heroName  → nombre
    nombre: apiData.heroName,
    // primaryRole → rol
    rol: apiData.primaryRole,
    // gamesPlayed → partidas
    partidas: apiData.gamesPlayed,
    // wins → victorias
    victorias: apiData.wins,
  };
}

// ahora mercy tiene el formato interno
const mercy = adaptarHeroeBlizzard(respuestaBlizzard);
// { nombre: 'Mercy', rol: 'support', partidas: 60, victorias: 39 }
console.log(mercy);

Si mañana Blizzard cambia gamesPlayed por totalGames, solo cambias el adapter. El resto del Team Builder no se entera.

Code smells y la regla del boy scout#

Un code smell no es un bug: es una señal de que el código se puede mejorar. Los smells más comunes:

  • Función enorme — más de 20-30 líneas suelen indicar varias responsabilidades mezcladas.
  • Nombre opacod, tmp, datos2 no dicen nada.
  • Número mágicoif (partidas >= 40) sin constante que explique el 40.
  • Comentario que explica el qué — si necesitas un comentario para explicar qué hace una línea, el nombre de la variable o función podría ser más claro.
  • Duplicación obvia — la misma lógica copiada en tres sitios.
  • Función con efectos ocultos — una función llamada calcularWinrate que también modifica el array original es una trampa.

La regla del boy scout: deja el código un poco mejor de como lo encontraste. No tienes que refactorizar el fichero entero cada vez que lo tocas. Pero si pasas por una función con un nombre opaco y te tomas diez segundos en mejorar el nombre, el proyecto va mejorando de forma gradual y sostenida.

Pruébalo#

El código siguiente muestra el before (el starter sucio) y el after (la versión limpia) produciendo exactamente la misma salida. Puedes editarlo y ver en la consola que refactorizar no cambia el comportamiento.


Con esto termina el Nivel 4. Has puesto en pie la maquinaria completa de un proyecto profesional: gestores de paquetes y lockfiles, el package.json y semver, Vite como entorno de desarrollo y de build, variables de entorno para separar configuración de código, linters y formateadores para mantener el estilo del equipo, y por último los principios que hacen que el código que va dentro de esa maquinaria sea mantenible. El Nivel 5, TypeScript, añade la red de seguridad que falta: tipos estáticos que atrapan errores antes de que el navegador los vea.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué dice el principio de responsabilidad única (SRP)?

Tu turno#

Tienes el código de hacerCosas funcionando. Refactorízalo hasta que sea heroesFiablesPorRol y cada helper tenga una sola responsabilidad. La salida de los dos console.log no debe cambiar ni un elemento.

Ejercicio · en esta página

Refactor del Team Builder

El código de `hacerCosas` funciona, pero nadie en el equipo lo entiende a primera vista. El starter usa `var` (código heredado); al refactorizar, modernízalo a `const`/`let`. Tu reto: refactorízalo hasta que `heroesFiablesPorRol(heroes, 'support')` devuelva exactamente lo mismo que `hacerCosas(heroes, 'support')`. La salida de los dos `console.log` no debe cambiar ni un carácter.

Paso 1: Nombres claros y métodos de array

  • La función se llama `heroesFiablesPorRol(lista, rol)`
  • Existe `winrateDe(heroe)` que calcula el winrate
  • Existe `etiquetaPorWinrate(winrate)` que devuelve 'estrella', 'solido' o 'flojo'
  • El sort a mano se sustituye por `.sort()`
  • La salida de los dos `console.log` es idéntica al starter
Ver soluciones
// Tier OK — clean code por nombres claros y helpers extraídos.
// El salto más grande que puedes dar en un refactor es renombrar bien.
// 'hacerCosas' pasa a 'heroesFiablesPorRol': ahora el nombre dice qué hace y qué devuelve.
// Los bloques anidados se extraen en funciones pequeñas con un nombre que se lee como una frase.
// Números mágicos (40, 0.6, 0.5) siguen inline: el tier siguiente los elimina.

// Datos: héroes del Team Builder.
const heroes = [
  { nombre: "Tracer", rol: "dps", partidas: 120, victorias: 70 },
  { nombre: "Reinhardt", rol: "tank", partidas: 90, victorias: 54 },
  { nombre: "Mercy", rol: "support", partidas: 60, victorias: 39 },
  { nombre: "Widowmaker", rol: "dps", partidas: 30, victorias: 10 },
  { nombre: "Ana", rol: "support", partidas: 80, victorias: 52 },
  { nombre: "Winston", rol: "tank", partidas: 20, victorias: 12 },
];

// Calcula el winrate de un héroe como proporción de victorias sobre partidas (0 a 1).
function winrateDe(heroe) {
  return heroe.victorias / heroe.partidas;
}

// Asigna la etiqueta que corresponde a este winrate: 'estrella', 'solido' o 'flojo'.
function etiquetaPorWinrate(winrate) {
  if (winrate >= 0.6) return "estrella";
  if (winrate >= 0.5) return "solido";
  return "flojo";
}

// Filtra los héroes fiables de un rol y los ordena por winrate. Nombre que dice QUÉ hace.
function heroesFiablesPorRol(lista, rol) {
  return (
    lista
      .filter(function (h) {
        return h.rol === rol && h.partidas >= 40;
      })
      .map(function (h) {
        // calcula el winrate del héroe
        const winrate = winrateDe(h);
        return {
          nombre: h.nombre,
          winrate: winrate,
          etiqueta: etiquetaPorWinrate(winrate),
        };
      })
      // mayor winrate primero
      .sort(function (a, b) {
        return b.winrate - a.winrate;
      })
  );
}

console.log(heroesFiablesPorRol(heroes, "support"));
console.log(heroesFiablesPorRol(heroes, "dps"));

Por qué este nivel

  • Renombras `hacerCosas` → `heroesFiablesPorRol` y `w` → `winrate`: ahora el nombre dice exactamente qué hace la función y qué contiene la variable. Eso ya es clean code real.
  • Extraes `winrateDe` y `etiquetaPorWinrate` y cambias el sort a mano por `.sort()`. Cada helper tiene un nombre que se lee como una frase. Misma salida: refactor de verdad, sin cambiar la conducta.
  • Los números 40, 0.6 y 0.5 siguen inline. No es ideal, pero el salto de calidad con solo renombrar y extraer es enorme. El siguiente nivel los elimina.