Funciones en Código Limpio: reglas y un ejemplo
Abres una función para cambiar una línea y te encuentras sesenta: bucles dentro de condiciones, un booleano que cambia medio comportamiento y un console.log perdido en mitad. Antes de tocar nada, tienes que leerla entera.
Una función debería funcionar como un electrodoméstico: la tostadora tuesta pan. No sabes cómo están soldadas las resistencias ni te hace falta; te basta con el nombre y el botón. Si además hiciera café y a veces mandara un correo, nadie se atrevería a enchufarla.
Este artículo resume las ideas del capítulo 3, «Functions», de Clean Code de Robert C. Martin (primera edición, 2008), explicadas con nuestras palabras, y las aplica a un ejemplo en JavaScript con tests.
Nombre: que diga qué hace
- Descriptivo y pronunciable. Si al leer el nombre sabes lo que hace la función, no necesitas abrirla.
- Un nombre largo y claro es mejor que uno corto y enigmático.
calcularTotalConDescuentogana acalc2. - Merece la pena dedicarle tiempo. Probar varios nombres y quedarte con el que mejor se lee es parte del trabajo. Con un IDE, renombrar cuesta un atajo.
- Verbo para la función, sustantivo para el argumento. Juntos se leen como una frase:
enviar(factura),escribirCampo(nombre). - Coherente. Usa la misma palabra para la misma idea en todo el código: si unas veces es
obtenery otrasrecuperarotraer, el lector duda de si hacen lo mismo.
Los nombres tienen su propio capítulo en el libro; lo tienes resumido en Nombres con sentido.
Tamaño: pequeñas, y más pequeñas todavía
- Cortas. Martin propone que una función casi nunca llegue a 20 líneas, y en sus ejemplos se quedan en muy pocas.
- Sin anidar. Un
ifdentro de unfordentro de otroifobliga a llevar varias cosas en la cabeza a la vez. Lo ideal son uno o dos niveles de sangría; el bloque de dentro de unifo de un bucle suele quedarse en una llamada a otra función con buen nombre. - Mejor muchas funciones pequeñas y obvias que una grande que hay que descifrar.
¿Por qué tan cortas? Martin Fowler lo explica con una regla que ayuda más que contar líneas: si tienes que pararte a estudiar un trozo de código para saber qué hace, extráelo a una función y ponle de nombre ese «qué». Separas la intención (qué hace) de la implementación (cómo lo hace).
Alcance: una sola cosa
- Una función hace una cosa, la hace bien y es lo único que hace.
- Cómo saber si hace más de una: intenta extraer otra función de ella. Si el nombre de la nueva no es una simple repetición de lo que ya hace el código, había dos cosas.
- Cuántos caminos tiene: una forma de medir si una función hace demasiado es contar sus caminos: su complejidad ciclomática.
- Un único nivel de abstracción. No mezcles en la misma función ideas de alto nivel («calcular el total del pedido») con detalles finos («multiplicar precio por cantidad y redondear a dos decimales»). Leer eso es como una receta que alterna «prepara la masa» con «gira la muñeca 30 grados».
Hacer algo o responder algo, no las dos cosas
- Sin efectos secundarios. Si la función se llama
comprobarContraseña, no debería además iniciar la sesión. Un efecto escondido es una sorpresa para quien la llama. - Separa órdenes y consultas. O cambia el estado de algo (una orden) o devuelve información (una consulta), pero no ambas. Así las consultas se pueden llamar sin miedo, las veces que haga falta. Esta idea viene de Bertrand Meyer, que la llamó command-query separation.
Tiene excepciones razonables: sacar el último elemento de una pila (pop) lo quita y lo devuelve a la vez, y Fowler reconoce que ahí prefiere saltarse la regla.
Argumentos: cuantos menos, mejor
| Número | Valoración en el libro |
|---|---|
| 0 | Lo ideal |
| 1 | Bien |
| 2 | Aceptable |
| 3 | Evítalo si puedes |
| Más de 3 | Necesita una justificación muy especial |
- Cada argumento multiplica los casos que hay que probar. Con tres argumentos, cubrir todas las combinaciones con tests se complica mucho.
- Argumento indicador (flag). Pasar un booleano, como
procesarPedido(pedido, true), anuncia que la función hace una cosa si estruey otra si esfalse. Mejor dos funciones, una para cada caso. Además, quien leetrueen la llamada no sabe qué significa. - Si necesitas muchos argumentos, probablemente varios forman un concepto propio: agrúpalos en un objeto (
dibujarCirculo(centro, radio)en vez dedibujarCirculo(x, y, radio)). - Evita los argumentos de salida, los que la función modifica para «devolver» algo. Si tiene que devolver algo, que lo devuelva.
Switch: solo si está escondido
Un switch (o una cadena de if/else sobre un tipo) tiende a repetirse por todo el código: cada vez que aparece un tipo nuevo, hay que buscar todos los switch y cambiarlos. El libro lo tolera cuando:
- aparece una sola vez,
- se usa para crear objetos polimórficos (una fábrica que decide qué clase instanciar),
- y queda oculto tras una relación de herencia, de modo que el resto del sistema no lo ve.
Errores: excepciones y funciones aparte
- Mejor lanzar excepciones que devolver códigos de error. Un código de error obliga a quien llama a comprobarlo al momento, con
ifanidados por todas partes. Cuándo conviene lanzar una excepción y cuándo es mejor decir en el tipo que no hay resultado lo tienes en devolver null o lanzar una excepción. - Tratar errores es una cosa. Una función que contiene un
try/catchno debería hacer nada más: eltryva al principio y no hay más código después delcatcho delfinally. Lo que va dentro deltry, a otra función.
Duplicación de código
El código duplicado es como tener la misma dirección apuntada en cinco agendas: cuando te mudas, siempre se te olvida una. Duplicar supone:
- el doble de riesgo de errores,
- el doble de mantenimiento,
- cambiar lo mismo en varios sitios con cada cambio,
- y un código más difícil de leer.
Evitarlo es el principio DRY, «no te repitas».
Una entrada y una salida: no hace falta un único return
La regla de «una sola entrada y una sola salida» viene de la programación estructurada de Edsger Dijkstra: un único return por función y nada de break, continue ni goto.
Martin la matiza: en funciones grandes tiene sentido, pero si tus funciones son pequeñas, varios return, un break o un continue no hacen daño y a veces se leen mejor. El goto sí conviene evitarlo. Una salida temprana suele aclarar el código:
const GASTOS_DE_ENVIO = 4;
function precioConEnvio(pedido) {
if (pedido.lineas.length === 0) return 0; // salida temprana: no hay nada que enviar
return totalPedido(pedido) + GASTOS_DE_ENVIO;
}
Regla descendente: se lee como un libro
El código debería leerse de arriba abajo como un texto: primero la función de más alto nivel, y justo debajo las funciones a las que llama, que bajan un nivel de abstracción. Y así sucesivamente, de lo más abstracto a lo más concreto a medida que bajas. Como un índice: primero los capítulos, luego los apartados.
Ejemplo: de una función que hace de todo a funciones limpias
Esta función calcula el total de un pedido, aplica un descuento si el cliente es VIP, modifica el pedido y escribe en la consola:
export function procesarPedido(pedido, esVip) {
let total = 0;
for (const linea of pedido.lineas) {
if (linea.cantidad > 0) {
total += linea.precio * linea.cantidad;
}
}
if (esVip) {
total = total * 0.9;
}
pedido.total = Math.round(total * 100) / 100;
console.log(`Pedido ${pedido.id}: ${pedido.total} €`);
return pedido.total;
}
Funciona, pero rompe varias reglas a la vez:
Lo que hace bien
- Calcula el total correcto
- Es corta
Lo que la ensucia
- Argumento indicador:
esVip - Efectos secundarios: modifica
pedidoy escribe en consola - Orden y consulta a la vez
- Mezcla niveles de abstracción y un número mágico (
0.9)
1. Primero, tests que fijen lo que ya hace
Antes de cambiar nada, se escriben tests que capturen el comportamiento actual. Son la red: si un paso del refactor rompe algo, te enteras al momento.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { procesarPedido } from './antes.js';
const pedido = () => ({
id: 7,
lineas: [
{ precio: 10, cantidad: 2 },
{ precio: 5, cantidad: 0 },
{ precio: 2.5, cantidad: 4 },
],
});
test('suma el precio por la cantidad de cada línea', () => {
assert.equal(procesarPedido(pedido(), false), 30);
});
test('aplica un 10 % de descuento a los clientes VIP', () => {
assert.equal(procesarPedido(pedido(), true), 27);
});
2. Después, pasos pequeños con los tests en verde
Se extrae una función cada vez y se ejecutan los tests después de cada paso. El resultado:
const DESCUENTO_VIP = 0.1;
export function totalPedido(pedido) {
return redondear(subtotal(pedido.lineas));
}
export function totalPedidoVip(pedido) {
return redondear(subtotal(pedido.lineas) * (1 - DESCUENTO_VIP));
}
export function ticket(pedido, total) {
return `Pedido ${pedido.id}: ${total} €`;
}
function subtotal(lineas) {
return lineas.filter(tieneUnidades).reduce((suma, linea) => suma + importe(linea), 0);
}
function tieneUnidades(linea) {
return linea.cantidad > 0;
}
function importe(linea) {
return linea.precio * linea.cantidad;
}
function redondear(euros) {
return Math.round(euros * 100) / 100;
}
Qué ha cambiado:
- El booleano se ha convertido en dos funciones:
totalPedidoytotalPedidoVip. La llamada ya dice lo que hace. - Solo hay consultas: ninguna modifica el pedido. Guardar el total o imprimir el ticket lo decide quien llama, en su sitio.
- Se lee de arriba abajo: primero lo que importa (el total) y debajo los detalles (subtotal, importe, redondeo).
- El
0.9tiene nombre:DESCUENTO_VIP. Más sobre esto en Números mágicos.
Los tests se adaptan a las nuevas funciones y se añade uno que comprueba que el pedido no cambia:
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { totalPedido, totalPedidoVip, ticket } from './pedido.js';
const pedido = () => ({
id: 7,
lineas: [
{ precio: 10, cantidad: 2 },
{ precio: 5, cantidad: 0 },
{ precio: 2.5, cantidad: 4 },
],
});
test('suma el precio por la cantidad de cada línea', () => {
assert.equal(totalPedido(pedido()), 30);
});
test('aplica un 10 % de descuento a los clientes VIP', () => {
assert.equal(totalPedidoVip(pedido()), 27);
});
test('no modifica el pedido que recibe', () => {
const original = pedido();
totalPedido(original);
assert.deepEqual(original, pedido());
});
test('compone el texto del ticket', () => {
assert.equal(ticket(pedido(), 30), 'Pedido 7: 30 €');
});
Para ejecutarlos no hace falta instalar nada: con Node.js, node --test busca y lanza los ficheros *.test.js. Con un package.json que tenga "type": "module" para poder usar import.
Cómo se escriben funciones limpias
Las funciones limpias no se escriben: se reescriben.
Martin cuenta que él tampoco las escribe así a la primera:
- Escribe un primer borrador que funcione, aunque sea largo, con bucles anidados y nombres provisionales.
- Cúbrelo con tests.
- Refactoriza poco a poco: divide funciones, cambia nombres, elimina duplicados y reordena.
- Ejecuta los tests después de cada cambio, para que sigan en verde.
Es un proceso, no un talento. Los tests son lo que permite refactorizar sin miedo.
Y si el código lo escribe una IA
Esta sección es criterio propio del enfoque IA Limpia, no algo que diga el libro.
Un asistente de IA genera en segundos funciones largas que hacen varias cosas, con un booleano «por si acaso» y un console.log dentro. Funcionan, y por eso es fácil aceptarlas sin mirar. Las reglas de este artículo sirven como lista de revisión:
- ¿Cada función hace una sola cosa y su nombre lo dice?
- ¿Hay booleanos como argumento que esconden dos funciones?
- ¿Alguna función cambia cosas que su nombre no anuncia?
- ¿Hay tests que fijen el comportamiento antes de pedirle que refactorice?
Para revisar un cambio entero de un agente, no solo sus funciones, la lista completa está en cómo revisar código generado por IA.
Pedirle a la IA el refactor en pasos pequeños, con los tests en verde en cada uno, es el mismo proceso de la sección anterior, solo que más rápido.
Fuentes
Comprobado el 7 de noviembre de 2026.
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, primera edición (Prentice Hall, 2008), capítulo 3, «Functions». Las ideas se explican con nuestras palabras; no hay citas literales.
- Segunda edición: ficha de Pearson (Addison-Wesley, 2025). Solo se ha consultado el índice, no el contenido.
- Martin Fowler, Function Length (2016): separar intención e implementación.
- Martin Fowler, Command Query Separation (2005): el origen en Bertrand Meyer y la excepción de
pop. - Node.js: Test runner (
node:test).
El ejemplo se ha ejecutado con Node.js 24.21.0 y node --test: los tests pasan contra la versión original y contra la refactorizada, y también el fragmento de salida temprana (con totalPedido de la versión refactorizada).