God Object: qué es este antipatrón y cómo evitarlo
Un God Object es una clase que concentra demasiadas responsabilidades: guarda datos, calcula, formatea, envía correos y decide por todos. Abres esa clase para cambiar el descuento de un cliente y te encuentras con mil líneas que también generan facturas. Tocas una línea y falla algo que no tenía nada que ver.
Qué es un God Object
Un God Object es un objeto muy grande que conoce demasiado o tiene demasiadas responsabilidades distintas. En los casos extremos, casi toda la funcionalidad del programa está escrita dentro de él, y el resto de las clases solo guardan datos que él maneja.
Es como la persona que en una empresa pequeña lo hace todo: lleva la contabilidad, atiende a los clientes, hace los repartos y guarda las llaves. Funciona mientras la empresa es pequeña. Pero cualquier cambio en cualquier área pasa por ella, y si se equivoca en una tarea, se resienten todas. El símil se queda corto en algo: en el código, repartir el trabajo no cuesta contratar a nadie; basta con mover código con cuidado.
Es un antipatrón (anti-pattern): una solución que parece cómoda al principio y que con el tiempo trae más problemas de los que resuelve.
God Object, God Class y Blob: ¿son lo mismo?
Sí, en la práctica nombran el mismo problema. Lo que cambia es de dónde viene cada nombre:
| Nombre | De dónde viene | Qué subraya |
|---|---|---|
| God Class y God Object | Arthur J. Riel, Object-Oriented Design Heuristics (1996), heurística 3.2 | Una clase que acapara la inteligencia del sistema |
| The Blob | William J. Brown y otros, AntiPatterns (1998) | Una clase que lo absorbe todo y crece sin parar, rodeada de clases que solo guardan datos |
| God Class como defecto medible | Michele Lanza y Radu Marinescu, Object-Oriented Metrics in Practice (2006) | Una forma de detectarla con métricas |
La tabla es un resumen nuestro. Las equivalencias sí están escritas: el libro Object-Oriented Reengineering Patterns, de Serge Demeyer, Stéphane Ducasse y Oscar Nierstrasz, dedica un patrón a dividir una God Class y le da como otros nombres «The Blob» y «God Class», citando el libro de AntiPatterns y el de Riel.
La heurística de Riel dice: «Do not create god classes/objects in your system. Be very suspicious of an abstraction whose name contains Driver, Manager, System, or Subsystem.» (no crees clases u objetos dios en tu sistema; desconfía mucho de una abstracción cuyo nombre contenga Driver, Manager, System o Subsystem; traducción nuestra). Usa las dos palabras a la vez: clase y objeto.
Si quieres afinar, hay un matiz: la clase es el molde que escribes y el objeto es lo que existe cuando el programa se ejecuta. El problema está en el diseño de la clase, así que «God Class» es algo más preciso. En el día a día se usan como sinónimos, y este artículo también lo hace.
Hay un término cercano que no es exactamente lo mismo: la clase demasiado grande (Large Class), que Martin Fowler recoge en Refactoring entre los «malos olores» del código. Un olor es una pista de que algo puede ir mal. Una clase grande suele ser un God Object, pero no siempre: puede ser larga y hacer una sola cosa. Esta distinción es interpretación nuestra.
Por qué es un problema
Incumple sobre todo el principio de responsabilidad única (SRP, por sus siglas en inglés), la S de los principios SOLID. Robert C. Martin lo define así: «each software module should have one and only one reason to change» (cada módulo debería tener una, y solo una, razón para cambiar; traducción nuestra). Y lo resume con una regla práctica: «Gather together the things that change for the same reasons. Separate those things that change for different reasons.» (junta lo que cambia por las mismas razones y separa lo que cambia por razones distintas; traducción nuestra).
Martin aclara en el mismo texto que esas razones son personas: quienes piden los cambios. Un God Object junta en un solo sitio lo que piden muchas personas distintas. De ahí salen sus problemas:
- Es frágil. Cualquier cambio puede provocar errores en otra parte de la clase.
- Cuesta mantenerlo. Para cambiar una línea tienes que entender cientos.
- Es difícil de probar. Para comprobar una regla sencilla tienes que montar todo lo que la clase necesita.
- Genera conflictos. Si cada tarea del equipo toca el mismo fichero, los conflictos al fusionar ramas son constantes.
Un God Object es una forma de deuda técnica: cada cambio cuesta más que el anterior.
Si todo cambia en el mismo sitio, todo se rompe en el mismo sitio.
Cómo detectar un God Object
No hay un número de líneas mágico. Busca varias de estas señales a la vez:
- El nombre es vago:
Gestor,Manager,Utils,Helper,System. Es la señal que ya daba Riel. Si no puedes darle un nombre concreto, probablemente hace demasiado. - No cabe en una frase: en Clases en código limpio se propone describir la clase en unas 25 palabras sin usar «y», «o», «si» ni «pero». Si no puedes, tiene varias responsabilidades.
- Tiene atributos que no se usan juntos: unos métodos usan unos campos y otros, otros. Son clases distintas que comparten fichero. Riel lo llama comportamiento que no se comunica y dice que es típico de las God Classes.
- Importa de todo: base de datos, correo, formato, red, configuración…
- Todo el mundo la cambia: aparece en casi todos los commits y en casi todos los conflictos.
- Sus tests necesitan una preparación enorme, o directamente no tiene tests.
Dos comandos te ayudan a encontrar candidatos en un repositorio Git. El primero lista los ficheros más largos; el segundo, los que más veces se han cambiado en los últimos seis meses:
# Los ficheros .js más largos (cambia la extensión por la de tu lenguaje)
find . -name node_modules -prune -o -name '*.js' -print0 | xargs -0 wc -l | sort -rn | head -10
# Los ficheros que más se han cambiado en los últimos 6 meses
git log --since="6 months ago" --format= --name-only | sort | uniq -c | sort -rn | head -10
Un fichero que está arriba en las dos listas merece que lo mires con calma. Para contar líneas con más detalle, por lenguaje y sin comentarios, tienes CLOC.
Con métricas: la regla GodClass de PMD
El analizador PMD, para Java, trae una regla llamada GodClass que aplica la estrategia de detección de Lanza y Marinescu. Marca una clase cuando se cumplen las tres condiciones a la vez:
- Mucha complejidad (WMC, la suma de la complejidad ciclomática de sus métodos).
- Mucho acceso a datos de otras clases (ATFD).
- Poca cohesión (TCC): sus métodos apenas comparten atributos.
En JavaScript no hay una regla equivalente en ESLint, pero puedes vigilar dos de esas señales con las reglas max-lines y complexity. Ninguna métrica sustituye a mirar el código: sirven para saber por dónde empezar.
Un ejemplo: antes del refactor
Los God Objects reales tienen miles de líneas. Este tiene treinta, pero muestra el problema: una clase que guarda pedidos, calcula precios, redacta la factura y la envía por correo. Los precios van en céntimos para evitar errores de redondeo con decimales.
// pedidos.js
export class GestorPedidos {
constructor() {
this.pedidos = [];
this.correosEnviados = [];
}
crearPedido(cliente, lineas) {
if (lineas.length === 0) throw new Error('El pedido no tiene líneas');
const pedido = { id: this.pedidos.length + 1, cliente, lineas };
this.pedidos.push(pedido);
this.correosEnviados.push({ para: cliente.email, texto: this.factura(pedido) });
return pedido;
}
total(pedido) {
const bruto = pedido.lineas.reduce((suma, l) => suma + l.precio * l.cantidad, 0);
return pedido.cliente.vip ? Math.round(bruto * 0.9) : bruto;
}
factura(pedido) {
const euros = (centimos) => `${(centimos / 100).toFixed(2)} €`;
return [
`Pedido ${pedido.id} para ${pedido.cliente.nombre}`,
...pedido.lineas.map((l) => `${l.cantidad} x ${l.producto}: ${euros(l.precio * l.cantidad)}`),
`Total: ${euros(this.total(pedido))}`,
].join('\n');
}
pedidosDe(email) {
return this.pedidos.filter((p) => p.cliente.email === email);
}
}
Piensa en quién podría pedir cambios en esta clase:
| Quién lo pide | Qué cambia | Dónde está hoy |
|---|---|---|
| Ventas | El descuento para clientes VIP | total |
| Contabilidad | El formato de la factura | factura |
| Sistemas | Cómo se envía el correo | crearPedido y correosEnviados |
| Quien gestiona los datos | Dónde y cómo se guardan los pedidos | pedidos y pedidosDe |
Cuatro razones para cambiar en una sola clase.
Cómo refactorizar un God Object paso a paso
Refactorizar es cambiar la estructura del código sin cambiar lo que hace. La clave está en esa segunda parte: para saber que no has cambiado lo que hace, necesitas tests que lo comprueben antes de tocar nada, y que sigan siendo exactamente los mismos al terminar.
- Protege el comportamiento actual con tests.
- Extrae una responsabilidad, deja que la clase delegue en ella y pasa los mismos tests.
- Repite con la siguiente hasta que la clase solo coordine.
- Después, y aparte, retira lo que solo quedaba por compatibilidad.
Es el mismo camino que describen Demeyer, Ducasse y Nierstrasz en su patrón para dividir una God Class: sacar poco a poco grupos de datos y el comportamiento que los usa, hasta que de la clase original solo queda una fachada (una puerta de entrada que reenvía cada llamada) que al final se puede retirar.
Pedidos solo guarda los pedidos y coordina; el precio, la factura y el envío del correo viven en sus propias piezas.1. Protege lo que ya funciona
Antes de mover nada, escribe tests que fijen cómo se comporta hoy la clase, aunque no te guste cómo está hecha. Se llaman tests de caracterización: no dicen cómo debería funcionar el código, sino cómo funciona ahora. Solo usan la interfaz pública de la clase, así que no dependen de cómo esté hecha por dentro. Este ejemplo usa el ejecutor de tests que trae Node, sin instalar nada:
// pedidos.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GestorPedidos } from './pedidos.js';
const ana = { nombre: 'Ana', email: '[email protected]', vip: false };
const luis = { nombre: 'Luis', email: '[email protected]', vip: true };
const cafe = { producto: 'Café', precio: 450, cantidad: 2 };
const te = { producto: 'Té', precio: 455, cantidad: 1 };
test('suma las líneas del pedido', () => {
const gestor = new GestorPedidos();
const pedido = gestor.crearPedido(ana, [cafe]);
assert.equal(gestor.total(pedido), 900);
});
test('aplica un 10 % de descuento a los clientes VIP', () => {
const gestor = new GestorPedidos();
const pedido = gestor.crearPedido(luis, [cafe]);
assert.equal(gestor.total(pedido), 810);
});
test('redondea al céntimo el descuento VIP', () => {
const gestor = new GestorPedidos();
const pedido = gestor.crearPedido(luis, [te]);
assert.equal(gestor.total(pedido), 410);
});
test('envía la factura al cliente al crear el pedido', () => {
const gestor = new GestorPedidos();
gestor.crearPedido(ana, [cafe]);
assert.deepEqual(gestor.correosEnviados, [
{ para: '[email protected]', texto: 'Pedido 1 para Ana\n2 x Café: 9.00 €\nTotal: 9.00 €' },
]);
});
test('busca los pedidos de un cliente', () => {
const gestor = new GestorPedidos();
gestor.crearPedido(ana, [cafe]);
gestor.crearPedido(luis, [cafe]);
assert.equal(gestor.pedidosDe('[email protected]').length, 1);
});
test('rechaza un pedido vacío', () => {
const gestor = new GestorPedidos();
assert.throws(() => gestor.crearPedido(ana, []), /no tiene líneas/);
});
Para que Node no tenga que adivinar el tipo de módulo, añade un package.json con {"type": "module"} y ejecuta:
node --test
✔ suma las líneas del pedido (0.961412ms)
✔ aplica un 10 % de descuento a los clientes VIP (0.211138ms)
✔ redondea al céntimo el descuento VIP (0.134854ms)
✔ envía la factura al cliente al crear el pedido (5.055859ms)
✔ busca los pedidos de un cliente (0.573722ms)
✔ rechaza un pedido vacío (0.540439ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 77.54789
Esa es tu red de seguridad. Desde aquí, el fichero pedidos.test.js no se toca hasta el final del refactor: lo ejecutas después de cada paso.
2. Extrae el cálculo del precio
Mueve la regla del precio a su propio módulo. La clase sigue teniendo el método total, pero ahora solo delega:
// precios.js
export function calcularTotal(pedido) {
const bruto = pedido.lineas.reduce((suma, l) => suma + l.precio * l.cantidad, 0);
return pedido.cliente.vip ? Math.round(bruto * 0.9) : bruto;
}
// pedidos.js (solo cambia esto)
import { calcularTotal } from './precios.js';
total(pedido) {
return calcularTotal(pedido);
}
node --test: seis de seis, con el mismo fichero de tests. Si algo hubiera fallado, sabrías qué paso lo ha roto, porque solo has cambiado una cosa.
3. Extrae la factura
Lo mismo con el texto de la factura:
// factura.js
const euros = (centimos) => `${(centimos / 100).toFixed(2)} €`;
export function textoFactura(pedido, total) {
return [
`Pedido ${pedido.id} para ${pedido.cliente.nombre}`,
...pedido.lineas.map((l) => `${l.cantidad} x ${l.producto}: ${euros(l.precio * l.cantidad)}`),
`Total: ${euros(total)}`,
].join('\n');
}
// pedidos.js (solo cambia esto)
import { textoFactura } from './factura.js';
factura(pedido) {
return textoFactura(pedido, this.total(pedido));
}
Otra vez seis de seis.
4. Saca el envío del correo
En vez de guardar los correos dentro de la clase, esta recibe un objeto que sabe enviarlos: un buzón. En los tests se usa uno que solo los apunta en memoria; en producción, otro con el mismo método enviar() que los envía de verdad.
// buzon.js
// Sustituto en memoria: en producción, otro objeto con el mismo método enviar().
export class BuzonEnMemoria {
constructor() {
this.enviados = [];
}
enviar(para, texto) {
this.enviados.push({ para, texto });
}
}
Para no romper a quien ya usa la clase, el constructor usa el buzón en memoria si no le pasas otro, y correosEnviados se mantiene como un acceso provisional. Así queda la clase después del refactor:
// pedidos.js
import { calcularTotal } from './precios.js';
import { textoFactura } from './factura.js';
import { BuzonEnMemoria } from './buzon.js';
export class GestorPedidos {
constructor(buzon = new BuzonEnMemoria()) {
this.buzon = buzon;
this.pedidos = [];
}
// Provisional: mantiene la interfaz antigua mientras haya código que la use.
get correosEnviados() {
return this.buzon.enviados;
}
crearPedido(cliente, lineas) {
if (lineas.length === 0) throw new Error('El pedido no tiene líneas');
const pedido = { id: this.pedidos.length + 1, cliente, lineas };
this.pedidos.push(pedido);
this.buzon.enviar(cliente.email, this.factura(pedido));
return pedido;
}
total(pedido) {
return calcularTotal(pedido);
}
factura(pedido) {
return textoFactura(pedido, this.total(pedido));
}
pedidosDe(email) {
return this.pedidos.filter((p) => p.cliente.email === email);
}
}
La prueba: los mismos tests, antes y después
El fichero de tests es idéntico al del paso 1; no ha cambiado ni un carácter. Su huella SHA-256 es la misma en las dos versiones:
sha256sum antes/pedidos.test.js despues/pedidos.test.js
7c1f90e35dc24a88509536628adb6bb39b6b8efa030f6742165041cb46a2545f antes/pedidos.test.js
7c1f90e35dc24a88509536628adb6bb39b6b8efa030f6742165041cb46a2545f despues/pedidos.test.js
Y con el código ya repartido en cuatro ficheros, node --test da:
✔ suma las líneas del pedido (0.954769ms)
✔ aplica un 10 % de descuento a los clientes VIP (1.230409ms)
✔ redondea al céntimo el descuento VIP (0.196971ms)
✔ envía la factura al cliente al crear el pedido (0.73244ms)
✔ busca los pedidos de un cliente (0.191221ms)
✔ rechaza un pedido vacío (0.369867ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 78.729567
Los mismos seis comportamientos, comprobados por los mismos tests, en verde antes y después.
Unos tests en verde solo valen si fallarían ante un error. Para comprobarlo, cambia a propósito el descuento de 0.9 a 0.8 en precios.js y vuelve a ejecutar:
✔ suma las líneas del pedido (0.951383ms)
✖ aplica un 10 % de descuento a los clientes VIP (1.970384ms)
✖ redondea al céntimo el descuento VIP (0.227439ms)
✔ envía la factura al cliente al crear el pedido (0.731158ms)
✔ busca los pedidos de un cliente (0.192383ms)
✔ rechaza un pedido vacío (0.339069ms)
ℹ tests 6
ℹ suites 0
ℹ pass 4
ℹ fail 2
Más abajo, Node explica los fallos: 720 !== 810 y 364 !== 410. Deshaz el cambio y prueba otro: quita el Math.round. Ahora solo falla un test, el del redondeo, con 409.5 !== 410; sin ese caso de importe impar, el redondeo podría desaparecer sin que nadie se enterase. La red funciona. Deshaz el cambio antes de seguir.
5. Después: retira la fachada
GestorPedidos ya solo reenvía llamadas. Cuando el resto del código llame directamente a calcularTotal y a textoFactura, puedes quitar esos métodos, pedir siempre el buzón en el constructor y cambiar el nombre vago GestorPedidos por Pedidos:
// pedidos.js
import { calcularTotal } from './precios.js';
import { textoFactura } from './factura.js';
export class Pedidos {
constructor(buzon) {
this.buzon = buzon;
this.pedidos = [];
}
crearPedido(cliente, lineas) {
if (lineas.length === 0) throw new Error('El pedido no tiene líneas');
const pedido = { id: this.pedidos.length + 1, cliente, lineas };
this.pedidos.push(pedido);
this.buzon.enviar(cliente.email, textoFactura(pedido, calcularTotal(pedido)));
return pedido;
}
pedidosDe(email) {
return this.pedidos.filter((p) => p.cliente.email === email);
}
}
Esto ya no es un refactor puro: cambia la interfaz pública, así que los tests cambian con ella. Hazlo como un paso aparte, con su propio commit. Cada test se va con lo que prueba: los del precio, a calcularTotal, sin crear pedidos ni correos; los demás, a Pedidos con un BuzonEnMemoria:
// precios.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { calcularTotal } from './precios.js';
const cafe = { producto: 'Café', precio: 450, cantidad: 2 };
const te = { producto: 'Té', precio: 455, cantidad: 1 };
test('suma las líneas del pedido', () => {
assert.equal(calcularTotal({ cliente: { vip: false }, lineas: [cafe] }), 900);
});
test('aplica un 10 % de descuento a los clientes VIP', () => {
assert.equal(calcularTotal({ cliente: { vip: true }, lineas: [cafe] }), 810);
});
test('redondea al céntimo el descuento VIP', () => {
assert.equal(calcularTotal({ cliente: { vip: true }, lineas: [te] }), 410);
});
// pedidos.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Pedidos } from './pedidos.js';
import { BuzonEnMemoria } from './buzon.js';
const ana = { nombre: 'Ana', email: '[email protected]', vip: false };
const luis = { nombre: 'Luis', email: '[email protected]', vip: true };
const cafe = { producto: 'Café', precio: 450, cantidad: 2 };
test('envía la factura al cliente al crear el pedido', () => {
const buzon = new BuzonEnMemoria();
new Pedidos(buzon).crearPedido(ana, [cafe]);
assert.deepEqual(buzon.enviados, [
{ para: '[email protected]', texto: 'Pedido 1 para Ana\n2 x Café: 9.00 €\nTotal: 9.00 €' },
]);
});
test('busca los pedidos de un cliente', () => {
const pedidos = new Pedidos(new BuzonEnMemoria());
pedidos.crearPedido(ana, [cafe]);
pedidos.crearPedido(luis, [cafe]);
assert.equal(pedidos.pedidosDe('[email protected]').length, 1);
});
test('rechaza un pedido vacío', () => {
const pedidos = new Pedidos(new BuzonEnMemoria());
assert.throws(() => pedidos.crearPedido(ana, []), /no tiene líneas/);
});
✔ envía la factura al cliente al crear el pedido (1.535804ms)
✔ busca los pedidos de un cliente (0.328058ms)
✔ rechaza un pedido vacío (2.262685ms)
✔ suma las líneas del pedido (0.976941ms)
✔ aplica un 10 % de descuento a los clientes VIP (0.21769ms)
✔ redondea al céntimo el descuento VIP (0.140585ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
Ahora cada petición de la tabla del principio cae en un único sitio:
Antes
- Una clase con cuatro razones para cambiar
- Para probar el descuento, creas pedidos y correos
- Cambiar la factura obliga a abrir la lógica de pedidos
Después
precios.js,factura.js,buzon.jsypedidos.js: una razón para cambiar cada uno- El descuento se prueba con una línea
- Cada cambio toca un fichero pequeño
Podrías seguir: sacar el almacenamiento de pedidos a su propio objeto, por ejemplo. Pero cuando lo necesites, no antes. El objetivo no es tener muchas clases, sino que cada una tenga un motivo claro para existir.
En un God Object real no extraerás funciones de tres líneas, sino grupos de métodos y atributos que siempre se usan juntos. En el catálogo de Fowler, lo que se ha hecho aquí corresponde a Extract Class (extraer clase) y a mover funciones a otro módulo.
Errores típicos al dividir un God Object
Esta lista es criterio nuestro, a partir de los pasos que proponen las fuentes del artículo:
- Reescribirlo de golpe. Una reescritura completa no tiene red: no sabes qué comportamiento se ha perdido hasta que alguien lo echa de menos.
- Cambiar los tests a la vez que el código. Si el test cambia en el mismo paso que el código, ya no demuestra que el comportamiento sea el mismo. Primero se mueve el código con los tests quietos; después, en otro paso, se reordenan los tests.
- Repartir por capas en vez de por motivos de cambio. Partir la clase en
GestorPedidosDatos,GestorPedidosLogicayGestorPedidosVistadeja tres clases con los mismos cuatro motivos de cambio mezclados. - Cambiar un God Object por un God Module. Si todo lo extraído acaba en un
utils.jsenorme, el problema solo ha cambiado de fichero. - Dejar la fachada para siempre. La fachada es un paso intermedio. Si se queda, la gente sigue añadiendo métodos en ella.
¿Alguna vez está justificado?
A veces se defiende que un objeto que lo centraliza todo da algo más de rendimiento en entornos muy limitados. En la mayoría de las aplicaciones actuales, repartir el código en varios objetos tiene un coste muy pequeño frente a lo que cuesta mantener una clase gigante; esto es criterio nuestro, no un dato medido. Si sospechas lo contrario en tu caso, mídelo antes de decidir.
Donde sí es razonable es en código que vas a tirar: un script de un solo uso o un prototipo para probar una idea. El problema empieza cuando ese prototipo se queda y empieza a crecer.
En Ruby on Rails, un modelo que acumula reglas de todo tipo es una forma habitual de God Object. Los concerns ayudan a ordenarlo, pero repartir el código en módulos que se incluyen en la misma clase no reduce sus motivos de cambio.
La IA y el God Object
Cuando le pides a un asistente de IA «añade X a esta clase», o simplemente «añade X» con la clase abierta, es probable que haga justo eso: meter X en la clase que tiene delante. Es el cambio más directo, el que menos ficheros toca y el que sigue el estilo del código que ve. Esto es criterio nuestro, no un dato medido: no conocemos un estudio que lo cuantifique. Pero encaja con cómo funcionan estas herramientas: trabajan con el contexto que les das y tienden a continuar lo que ya existe.
Así, un God Object puede crecer más rápido. Cada petición pequeña es razonable; la suma, no. Tres hábitos ayudan:
- Di dónde va el cambio. En vez de «añade el descuento por volumen», pide «crea un módulo para el descuento por volumen, con sus tests, y llámalo desde
calcularTotal». Si tu proyecto tiene un fichero de instrucciones para el agente, deja escrita ahí la regla: una responsabilidad por clase y nada nuevo en las clases que ya son grandes. - Pide el refactor como en este artículo. Primero los tests de caracterización; después, una extracción cada vez, con los mismos tests en verde. Si el agente cambia los tests en el mismo paso, el resultado no demuestra nada. Un ejemplo completo, en doce pasos: refactorizar con IA.
- Revisa el diff con una pregunta: ¿cuántas razones para cambiar tiene ahora esta clase? Si ha ganado una, pide que la extraiga antes de aceptar el cambio.
Es la idea de fondo de la IA Limpia: la IA acelera lo que ya tienes. Si tu código está ordenado, te ayuda a mantenerlo así; si ya hay un God Object, lo engorda.
Tu siguiente paso
Ejecuta los dos comandos de la sección de detección en tu proyecto y elige el fichero que salga arriba en las dos listas. No lo rehagas entero: escribe tests de caracterización para una sola responsabilidad y extráela. La próxima vez que lo toques, saca otra. Es la regla del Boy Scout aplicada a un God Object: déjalo un poco mejor cada vez que pases por él.
Fuentes
Consultadas el 7 de noviembre de 2026 y, las de Riel, Demeyer y otros, PMD y ESLint, el 9 de noviembre de 2026.
- Arthur J. Riel: resumen de las heurísticas de Object-Oriented Design Heuristics (Addison-Wesley, 1996), publicado por el propio autor en la lista patterns-discussion el 3 de julio de 1996. De aquí sale la cita de la heurística 3.2. No se ha consultado el libro.
- Serge Demeyer, Stéphane Ducasse y Oscar Nierstrasz: «Split Up God Class», en Object-Oriented Reengineering Patterns (licencia CC BY-SA). Recoge The Blob y God Class como otros nombres y describe la división hasta dejar una fachada.
- William J. Brown, Raphael C. Malveau, Hays W. McCormick III y Thomas J. Mowbray: AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis (Wiley, 1998). Origen del nombre The Blob. No se ha consultado el libro; la equivalencia con God Class se toma de Demeyer, Ducasse y Nierstrasz.
- PMD: regla
GodClassy definición de sus métricas, documentación de PMD 7, que remite a Michele Lanza y Radu Marinescu, Object-Oriented Metrics in Practice (Springer, 2006). No se ha consultado el libro. - ESLint: reglas
max-linesycomplexity. - Robert C. Martin: «The Single Responsibility Principle» (8 de mayo de 2014). De aquí salen las citas literales del principio de responsabilidad única.
- Clare Sudbery: «Refactoring: This class is too large», en martinfowler.com (14 de abril de 2020).
- Martin Fowler: Refactoring: Improving the Design of Existing Code, 2.ª edición (Addison-Wesley, 2018), y su catálogo en línea: Extract Class. No se cita textualmente.
- Node.js: ejecutor de tests
node:testynode:assert.
Todo el código del ejemplo se ha ejecutado paso a paso con Node.js 24.21.0 en Linux; las salidas que se muestran son las reales. Los comandos de detección se han probado con Git 2.53.0 y GNU coreutils.