30 KiB
Prácticas de código limpio (Clean Code)
Parte de esta guía se basa en conceptos del resumen del curso "Clean Code" de Academind GmbH / Maximilian Schwarzmüller, © 2020.
Este documento desarrolla las prácticas de código limpio definidas en el SKILL.md principal. Sus recomendaciones deben aplicarse con criterio profesional, priorizando la corrección, claridad, simplicidad y mantenibilidad del software.
Las métricas y reglas estructurales descritas a continuación son orientaciones de diseño, salvo que las convenciones específicas del proyecto establezcan requisitos más estrictos.
1. ¿Qué es el código limpio?
Es código que resulta:
- Fácil de comprender: expresa claramente su intención.
- Fácil de modificar: permite realizar cambios localizados y controlados.
- Fácil de probar: facilita verificar su comportamiento y controlar sus dependencias.
- Sencillo: evita complejidad, abstracciones y mecanismos innecesarios.
- Mantenible: facilita corregir errores y evolucionar funcionalidades sin introducir riesgos desproporcionados.
El código limpio no es necesariamente el más corto ni el que utiliza más patrones de diseño.
El objetivo es minimizar la complejidad global del sistema, no optimizar métricas aisladas.
2. Enfoque centrado en las personas
El software tiene tres grupos principales de destinatarios:
- Usuarios: necesitan que el producto satisfaga sus necesidades.
- Clientes: esperan obtener valor económico, operativo o estratégico.
- Desarrolladores: deben comprender, mantener y evolucionar el software.
El código suele leerse muchas más veces de las que se escribe.
Por tanto, debe favorecer la comprensión y el mantenimiento sin perjudicar las necesidades funcionales del producto.
La calidad debe evaluarse considerando tanto el comportamiento observable como el coste de evolución del sistema.
3. Principios de nomenclatura
3.1. Consistencia y unicidad
Prioridad máxima: un mismo concepto debe tener un nombre coherente dentro de su contexto.
Evita utilizar términos diferentes para representar exactamente la misma operación o responsabilidad.
// MAL: Nombres inconsistentes para una misma operación.
getUserById(id);
fetchCustomerById(id);
retrieveClientById(id);
// BIEN: Convención coherente para operaciones equivalentes.
getUser(id);
getOrder(id);
getProduct(id);
No obstante, distingue correctamente los conceptos del dominio. Por ejemplo, User, Customer y Client no deben tratarse como sinónimos si representan entidades diferentes.
Mantén las convenciones existentes de un módulo cuando sean coherentes y no contradigan requisitos superiores.
3.2. Comprensibilidad
Utiliza nombres que expresen el significado del dominio.
// MAL: Nombre genérico.
const arr = users.filter((user) => user.isActive);
// BIEN: Expresa claramente el resultado.
const activeUsers = users.filter((user) => user.isActive);
No utilices terminología de negocio incorrecta únicamente para evitar términos técnicos.
3.3. Especificidad
Evita nombres ambiguos como data, info, manager, handler, processor o utils cuando no permitan identificar la responsabilidad real.
// MAL: Responsabilidades imprecisas.
class DataManager {}
function processInfo(data: unknown): void {}
// BIEN: Responsabilidades identificables.
class OrderRepository {}
function validatePayment(payment: Payment): void {}
Estos términos no están prohibidos. Pueden resultar adecuados cuando describan correctamente el concepto, como EventHandler o ImageProcessor.
3.4. Brevedad sin perder claridad
Los nombres deben ser concisos, pero suficientemente descriptivos.
// MAL: Abreviaturas crípticas.
const usrLst = getUsrs();
// MAL: Nombre innecesariamente largo.
const listOfAllActiveUsersInTheSystem = getActiveUsers();
// BIEN: Breve y claro.
const activeUsers = getActiveUsers();
3.5. Facilidad de búsqueda
Utiliza nombres suficientemente específicos para facilitar su localización.
// MAL: Nombre demasiado genérico.
const data = fetch();
// BIEN: Identifica el contenido.
const orderSummary = fetchOrderSummary();
3.6. Facilidad de pronunciación
Los identificadores deben poder comunicarse con claridad entre desarrolladores.
// MAL: Abreviatura difícil de interpretar.
const genymdhms = generateTimestamp();
// BIEN: Concepto comprensible.
const timestamp = generateTimestamp();
3.7. Evitar términos redundantes
No añadas palabras que repitan información evidente.
// MAL: Redundancia innecesaria.
const userData = user;
class UserClass {}
// BIEN: Nombres directos.
const currentUser = user;
class User {}
4. Funciones y métodos
Las funciones deben ser comprensibles, cohesionadas y expresar una responsabilidad concreta.
4.1. Minimizar los parámetros
Un número reducido de parámetros suele facilitar la lectura y el uso de una función.
Sin embargo, no existe un número máximo universalmente correcto.
// Operación sin parámetros.
createSession();
// Operación con un parámetro.
isValidEmail(email);
// Operación con dos parámetros.
calculateTotal(price, quantity);
// Operación con varios parámetros relacionados.
createUser({
firstName: 'Max',
lastName: 'Smith',
email: 'test@example.com',
role: 'editor',
age: 31,
});
Agrupa parámetros cuando formen una unidad conceptual o mejoren la legibilidad.
interface Rectangle {
x: number;
y: number;
width: number;
height: number;
}
function createRectangle(rectangle: Rectangle): void {
// Crear el rectángulo.
}
createRectangle({
x: 10,
y: 9,
width: 30,
height: 12,
});
No crees objetos de parámetros innecesarios para funciones sencillas con dos o tres argumentos claros.
4.2. Mantener las funciones pequeñas y cohesionadas
Una función debe tener una responsabilidad identificable.
Evita mezclar validaciones, acceso a datos, decisiones de negocio y efectos externos cuando exista una separación clara y útil.
// MAL: Mezcla responsabilidades y compara contraseñas directamente.
async function login(email: string, password: string): Promise<void> {
if (!email.includes('@')) {
throw new Error('Correo electrónico no válido');
}
const user = await database.findUserByEmail(email);
if (!user || user.password === password) {
throw new Error('Credenciales no válidas');
}
await sessionService.create(user.id);
}
Además de mezclar responsabilidades, el ejemplo anterior contiene un error lógico en la comprobación de las credenciales y presupone una comparación directa de contraseñas.
Una implementación más clara delega las operaciones específicas:
async function login(
email: string,
password: string,
): Promise<void> {
validateLoginInput(email, password);
const user = await findUserByEmail(email);
if (!user) {
throw new Error('Credenciales no válidas');
}
await verifyCredentials(user, password);
await createUserSession(user.id);
}
Este ejemplo presupone que findUserByEmail devuelve Promise<User | null> y que verifyCredentials rechaza la promesa cuando la contraseña no es válida. Solo se crea la sesión después de verificar las credenciales. Adapta el error genérico a la política del proyecto sin filtrar si una cuenta existe.
La verificación de credenciales debe utilizar el mecanismo seguro establecido por el proyecto.
No extraigas funciones únicamente para reducir líneas. Cada extracción debe aportar significado, reutilización o una separación útil de responsabilidades.
4.3. Un nivel de abstracción coherente
Las operaciones dentro de una función deben mantener un nivel de abstracción razonablemente uniforme.
// MAL: Mezcla configuración técnica y operación de negocio.
async function printDocument(documentPath: string): Promise<void> {
const options = { encoding: 'utf8' as const };
const content = await fileSystem.readFile(documentPath, options);
const printer = new Printer('pdf');
await printer.print(content);
}
// BIEN: Las operaciones expresan la intención.
async function printDocument(documentPath: string): Promise<void> {
const document = await readDocument(documentPath);
await printPdfDocument(document);
}
No es necesario crear una función auxiliar para cada instrucción. El objetivo es evitar que los detalles técnicos oculten el flujo principal.
4.4. Dividir funciones con criterio
Evita las extracciones artificiales.
Señales de una división innecesaria:
- La función extraída únicamente cambia el nombre de una operación trivial.
- Comprender el flujo requiere navegar entre demasiadas funciones.
- No existe un nombre significativo para la nueva función.
// EXCESIVAMENTE FRAGMENTADO.
async function createUser(email: string): Promise<void> {
await saveUser(email);
}
async function saveUser(email: string): Promise<void> {
const user = buildUser(email);
await userRepository.save(user);
}
function buildUser(email: string): User {
return new User(email);
}
// MÁS DIRECTO.
async function createUser(email: string): Promise<void> {
const user = new User(email);
await userRepository.save(user);
}
Extrae código cuando:
- Represente una responsabilidad diferenciada.
- Oculte complejidad técnica significativa.
- Exprese una operación relevante del dominio.
- Facilite las pruebas o el mantenimiento.
- Permita reutilizar comportamiento sin crear acoplamientos artificiales.
4.5. Evitar efectos secundarios inesperados
Los efectos secundarios deben ser coherentes con el nombre y el contrato de la operación.
// MAL: Una validación crea una sesión.
async function validateCredentials(
email: string,
password: string,
): Promise<void> {
await verifyCredentials(email, password);
await createSession();
}
// BIEN: La operación expresa que inicia una sesión.
async function authenticateAndStartSession(
email: string,
password: string,
): Promise<void> {
await verifyCredentials(email, password);
await createSession();
}
Cuando sea posible, separa consultas, validaciones y modificaciones de estado.
No obstante, una operación puede combinar responsabilidades técnicas necesarias para satisfacer un caso de uso, siempre que su contrato sea claro.
5. Estructuras de control
5.1. Priorizar condiciones comprensibles
Las condiciones positivas suelen resultar más directas, pero no deben imponerse universalmente.
// Claro: expresa directamente la condición.
if (isEmpty(content)) {
throw new Error('El contenido está vacío');
}
// También correcto: negación natural del estado esperado.
if (!isOpen(transaction)) {
throw new Error('La transacción no está abierta');
}
Selecciona la expresión que mejor comunique la intención.
5.2. Evitar el anidamiento excesivo
El anidamiento profundo dificulta seguir el flujo de ejecución.
Utiliza cláusulas de guarda y retornos anticipados cuando mejoren la claridad.
// MAL: Anidamiento innecesario.
async function messageUser(
user: User | null,
message: string,
): Promise<void> {
if (user) {
if (message) {
if (user.acceptsMessages) {
const success = await user.sendMessage(message);
if (success) {
console.log('Mensaje enviado');
}
}
}
}
}
// BIEN: Cláusula de guarda.
async function messageUser(
user: User | null,
message: string,
): Promise<void> {
if (!user || !message || !user.acceptsMessages) {
return;
}
const success = await user.sendMessage(message);
if (success) {
console.log('Mensaje enviado');
}
}
El objetivo es mejorar la lectura sin alterar el comportamiento.
5.3. Extraer estructuras de control complejas
Extrae una estructura de control cuando represente un comportamiento independiente.
async function connectDatabase(uri: string): Promise<Connection> {
validateDatabaseUri(uri);
try {
return await connectPrimaryDatabase(uri);
} catch (error) {
if (!isRecoverableConnectionError(error)) {
throw error;
}
return connectFallbackDatabase(uri);
}
}
Este ejemplo presupone que el sistema admite una conexión alternativa.
La recuperación ante errores debe responder a una necesidad real y no ocultar fallos de configuración o seguridad.
No introduzcas funciones auxiliares cuando el condicional sea más claro directamente.
5.4. Utilizar polimorfismo cuando aporte valor
Los condicionales repetidos pueden indicar que existe una variación de comportamiento.
Sin embargo, no todo if o switch necesita sustituirse por polimorfismo.
interface PaymentProcessor {
processPayment(transaction: Transaction): Promise<void>;
processRefund(transaction: Transaction): Promise<void>;
}
class CreditCardProcessor implements PaymentProcessor {
async processPayment(transaction: Transaction): Promise<void> {
await processCreditCardPayment(transaction);
}
async processRefund(transaction: Transaction): Promise<void> {
await processCreditCardRefund(transaction);
}
}
class PayPalProcessor implements PaymentProcessor {
async processPayment(transaction: Transaction): Promise<void> {
await processPayPalPayment(transaction);
}
async processRefund(transaction: Transaction): Promise<void> {
await processPayPalRefund(transaction);
}
}
Una factoría o registro puede seleccionar el procesador adecuado según el método de pago.
Este enfoque resulta útil cuando hay varias implementaciones, comportamiento significativo o posibilidades reales de extensión.
Para dos alternativas sencillas y estables, un condicional puede ser más claro.
5.5. Gestión de errores
Utiliza excepciones para fallos excepcionales y resultados tipados cuando representen mejor errores de negocio esperados.
Ejemplo con excepciones:
async function createUser(email: string): Promise<User> {
validateEmail(email);
const existingUser = await userRepository.findByEmail(email);
if (existingUser) {
throw new EmailAlreadyExistsError(email);
}
return userRepository.save(new User(email));
}
Ejemplo con resultados tipados:
type CreateUserResult =
| { success: true; user: User }
| { success: false; reason: 'EMAIL_ALREADY_EXISTS' };
Ambos enfoques pueden ser correctos según la arquitectura.
Evita:
- Capturar excepciones y descartarlas silenciosamente.
- Usar excepciones indiscriminadamente como flujo normal.
- Devolver códigos ambiguos sin significado.
- Registrar información sensible.
- Ocultar el motivo real de un fallo.
6. Clases y objetos
6.1. Objetos frente a contenedores de datos
Distingue entre:
Objetos: encapsulan estado y exponen comportamiento.
Contenedores de datos: representan y transportan información.
// Contenedor de datos.
interface UserData {
name: string;
age: number;
}
// Objeto que encapsula comportamiento.
class User {
constructor(
private readonly name: string,
private readonly age: number,
) {}
greet(): string {
return `Hola, soy ${this.name} y tengo ${this.age} años.`;
}
}
Ambos diseños son válidos.
Utiliza estructuras de datos para DTO, respuestas de API, modelos de presentación y otros escenarios donde exponer información sea su finalidad.
Utiliza objetos con comportamiento cuando sea necesario proteger invariantes o encapsular reglas del dominio.
Evita mezclar responsabilidades sin una justificación clara, pero no prohíbas que una entidad exponga datos necesarios mediante métodos o propiedades controladas.
6.2. Cohesión de clases
La cohesión describe hasta qué punto los elementos de una clase colaboran para cumplir una responsabilidad común.
Una clase cohesionada:
- Tiene una finalidad reconocible.
- Agrupa operaciones relacionadas.
- Mantiene un estado coherente.
- Presenta motivos de cambio relacionados.
Una cohesión aparentemente baja puede indicar responsabilidades mezcladas, pero no debe evaluarse únicamente contando cuántos atributos utiliza cada método.
Las clases sin estado, como determinados servicios, también pueden tener una cohesión elevada.
Revisa las clases cuyo comportamiento no pueda describirse mediante una responsabilidad clara.
7. Object Calisthenics: nueve heurísticas de diseño
Object Calisthenics es un conjunto de ejercicios orientados a mejorar el diseño orientado a objetos.
Sus reglas pueden aplicarse estrictamente durante ejercicios de aprendizaje, pero no deben convertirse automáticamente en restricciones obligatorias de producción.
7.1. Reducir los niveles de indentación
Regla original: un nivel de indentación por método.
Aplicación recomendada: mantener los flujos comprensibles y reducir el anidamiento cuando resulte excesivo.
function processOrders(orders: Order[]): void {
for (const order of orders) {
if (!order.isValid()) {
continue;
}
processOrder(order);
}
}
function processOrder(order: Order): void {
for (const item of order.items) {
if (!item.inStock) {
continue;
}
processItem(item);
}
}
No es necesario eliminar todos los niveles de anidamiento si la alternativa perjudica la claridad.
7.2. Evitar else cuando sea innecesario
Regla original: no utilizar else.
Aplicación recomendada: preferir retornos anticipados cuando simplifiquen el flujo.
function getDiscount(user: User): number {
if (user.isPremium) {
return 20;
}
return 0;
}
Utilizar else sigue siendo válido cuando representa claramente dos ramas complementarias.
No introduzcas funciones auxiliares o lógica artificial únicamente para eliminar esta palabra clave.
7.3. Encapsular primitivas con significado de dominio
Regla original: encapsular todos los valores primitivos.
Aplicación recomendada: utilizar objetos de valor cuando aporten validación, invariantes, operaciones o semántica.
class Email {
constructor(readonly value: string) {
if (!isValidEmail(value)) {
throw new InvalidEmailError();
}
}
}
class Age {
constructor(readonly value: number) {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new InvalidAgeError();
}
}
}
function createUser(email: Email, age: Age): User {
return new User(email, age);
}
No encapsules todos los números, cadenas o booleanos de forma indiscriminada.
En TypeScript, considera branded types cuando únicamente necesites diferenciar identificadores.
7.4. Colecciones de primera clase
Regla original: una clase que contenga una colección no debe tener otros atributos.
Aplicación recomendada: encapsular colecciones cuando tengan comportamiento o invariantes propios.
class OrderItems {
private readonly items: OrderItem[];
constructor(items: readonly OrderItem[] = []) {
this.items = [...items];
}
add(item: OrderItem): void {
this.items.push(item);
}
total(): Money {
return calculateOrderTotal(this.items);
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
class Order {
constructor(
readonly id: OrderId,
private readonly items: OrderItems,
) {}
}
La copia del constructor protege la estructura del array frente a cambios a través de la referencia original. Es superficial: si los elementos contienen estado mutable que pueda comprometer invariantes, define también su propiedad, inmutabilidad o copia adecuada. readonly no convierte automáticamente los objetos anidados en inmutables.
Utiliza arrays y colecciones nativas cuando sean suficientes.
7.5. Ley de Deméter
Regla original: un único punto por línea.
Aplicación recomendada: evitar que un componente dependa de los detalles internos de estructuras ajenas.
// Acoplamiento a la estructura interna.
const city = order.customer.address.city;
// El dominio proporciona la operación necesaria.
const city = order.getShippingCity();
El segundo enfoque resulta adecuado cuando obtener la ciudad de envío constituye una responsabilidad del pedido.
No obstante, acceder a datos públicos de DTO o utilizar APIs fluidas puede ser perfectamente legítimo.
7.6. Evitar abreviaturas innecesarias
Regla original: no utilizar abreviaturas.
Aplicación recomendada: evitar abreviaturas ambiguas y mantener las ampliamente reconocidas.
// MAL.
const custRepo = new CustRepo();
const ord = new Ord();
// BIEN.
const customerRepository = new CustomerRepository();
const order = new Order();
Abreviaturas habituales como id, url, api o http pueden utilizarse cuando sean coherentes con las convenciones del proyecto.
7.7. Mantener entidades pequeñas
Regla original:
- Clases de menos de 50 líneas.
- Métodos de menos de 10 líneas.
- Archivos de menos de 100 líneas.
Aplicación recomendada:
- Revisa los métodos extensos cuando acumulen complejidad o responsabilidades.
- Como orientación, presta atención a los métodos que superen aproximadamente 10-20 líneas.
- Revisa las clases grandes cuando mezclen responsabilidades independientes.
- Organiza los archivos para facilitar su navegación.
- No establezcas límites absolutos de líneas como criterio de calidad.
Una implementación clara de 25 líneas puede ser preferible a cinco funciones artificiales de cinco líneas.
7.8. Limitar el estado de las clases
Regla original: no utilizar más de dos variables de instancia.
Aplicación recomendada: mantener un estado coherente con la responsabilidad de la clase.
class Order {
constructor(
readonly id: OrderId,
readonly customerId: CustomerId,
readonly items: OrderItems,
readonly status: OrderStatus,
) {}
}
Esta clase puede ser correcta aunque tenga cuatro atributos.
Evalúa si el estado pertenece al mismo concepto, no si supera un límite numérico.
Extrae objetos cuando representen subconceptos reales y no únicamente para reducir el número de propiedades.
7.9. Favorecer comportamiento frente a acceso indiscriminado al estado
Regla original: no utilizar getters, setters ni propiedades públicas.
Aplicación recomendada: encapsular las operaciones que deban preservar invariantes.
// Riesgo: La lógica de retirada queda fuera del objeto.
if (account.balance >= amount) {
account.balance -= amount;
}
// Mejor: La cuenta controla sus reglas.
class Account {
constructor(private balance: Money) {
if (balance.isNegative()) {
throw new InvalidInitialBalanceError();
}
}
withdraw(amount: Money): WithdrawResult {
if (!amount.isPositive()) {
return WithdrawResult.invalidAmount();
}
if (!this.balance.isGreaterThanOrEqualTo(amount)) {
return WithdrawResult.insufficientFunds();
}
this.balance = this.balance.subtract(amount);
return WithdrawResult.success();
}
}
El ejemplo presupone un Money inmutable con comparación y resta entre monedas compatibles, y los resultados success, invalidAmount e insufficientFunds. Mantiene las mismas comprobaciones de importe y saldo inicial que BankAccount en diseño-de-objetos.md. Si falla una validación o una operación monetaria, no debe modificarse el saldo.
Los getters y propiedades de solo lectura siguen siendo adecuados cuando exponen información sin comprometer invariantes.
Evita setters que permitan introducir estados inválidos.
8. Comentarios
8.1. Explicar decisiones y motivos
Los comentarios deben aportar información que no resulte evidente al leer el código.
Prioriza explicaciones sobre:
- Decisiones de diseño.
- Restricciones de negocio.
- Compatibilidad.
- Riesgos.
- Contratos.
- Comportamientos inesperados.
- Soluciones temporales justificadas.
// MAL: Repite la operación.
counter++;
// BIEN: Explica una particularidad relevante.
// La API heredada utiliza índices basados en cero.
const displayIndex = apiIndex + 1;
8.2. Preferir código expresivo
Cuando sea posible, utiliza nombres y abstracciones claras en lugar de comentarios que expliquen condiciones complejas.
// MENOS CLARO.
if (user.subscriptionLevel >= 2 && !user.isBanned) {
// Permitir acceso a funciones premium.
}
// MÁS CLARO.
if (user.canAccessPremiumFeatures()) {
enablePremiumFeatures();
}
No obstante, la documentación pública, los comentarios sobre decisiones técnicas y las explicaciones de contratos pueden ser necesarios incluso con código bien nombrado.
8.3. Comentarios que deben evitarse
- Comentarios redundantes.
- Explicaciones que repiten literalmente el código.
- Comentarios obsoletos.
- Información que debería expresarse mediante tipos o nombres.
- Código comentado que ya no se utiliza.
Los comentarios TODO deben identificar tareas concretas y, cuando el proyecto lo requiera, enlazar con su seguimiento correspondiente.
9. Formato y organización
9.1. Organización vertical
- Mantén juntos los elementos relacionados.
- Separa conceptos mediante líneas vacías cuando mejore la legibilidad.
- Agrupa propiedades, métodos y firmas de sobrecarga relacionadas.
- Mantén una organización coherente entre archivos.
- Sitúa las operaciones públicas y los detalles internos según las convenciones del proyecto.
9.2. Organización horizontal
- Utiliza una indentación consistente.
- Mantén espacios coherentes alrededor de operadores.
- Evita líneas innecesariamente extensas.
- Como orientación, considera longitudes aproximadas de 80-120 caracteres.
- Respeta la configuración del formateador oficial del proyecto.
No introduzcas modificaciones de formato ajenas a la tarea.
9.3. Organización narrativa
Cuando resulte apropiado, estructura el código desde operaciones de alto nivel hacia detalles de implementación.
class OrderProcessor {
async process(order: Order): Promise<ProcessResult> {
this.validate(order);
const total = this.calculateTotal(order);
return this.save(order, total);
}
private validate(order: Order): void {
// Validar invariantes y requisitos.
}
private calculateTotal(order: Order): Money {
return order.calculateTotal();
}
private async save(
order: Order,
total: Money,
): Promise<ProcessResult> {
return this.repository.save(order, total);
}
}
El ejemplo ilustra la organización narrativa y presupone las dependencias y contratos correspondientes.
No es obligatorio colocar todos los métodos privados detrás de los públicos si las convenciones del proyecto establecen otra organización.
9.4. Convenciones del proyecto
Cuando el proyecto no establezca otra norma, aplica estas convenciones generales. Las preferencias adicionales y la prioridad de instrucciones se mantienen en el SKILL.md principal:
- Identificadores en inglés.
- Comentarios y documentación en castellano.
- Archivos y directorios en
kebab-case. - Clases, tipos e interfaces en
PascalCase. - Variables, funciones y métodos en
camelCase. - Constantes globales en
UPPER_SNAKE_CASE. - Llaves obligatorias en estructuras de control.
- Respeto de las convenciones existentes y de las herramientas de formato configuradas.
10. Lista de comprobación de código limpio
Nomenclatura
- ¿Los identificadores expresan claramente su significado?
- ¿Utilizo términos coherentes con el dominio?
- ¿Los nombres son suficientemente específicos?
- ¿He evitado abreviaturas ambiguas?
- ¿Las variables booleanas expresan condiciones claras?
- ¿Se mantienen las convenciones del proyecto?
Funciones y métodos
- ¿Cada función tiene una responsabilidad coherente?
- ¿Los parámetros son necesarios y comprensibles?
- ¿He agrupado parámetros cuando representan un concepto común?
- ¿Las operaciones mantienen un nivel de abstracción razonable?
- ¿He evitado dividir funciones artificialmente?
- ¿Los efectos secundarios son previsibles?
- ¿El código expresa claramente su intención?
Estructuras de control
- ¿Las condiciones son comprensibles?
- ¿Puedo reducir el anidamiento sin complicar el código?
- ¿Las cláusulas de guarda aportan claridad?
- ¿Los condicionales complejos justifican una extracción?
- ¿He evitado introducir polimorfismo innecesario?
- ¿El tratamiento de errores es adecuado al contexto?
Objetos y clases
- ¿La clase tiene una responsabilidad identificable?
- ¿Su estado es coherente con esa responsabilidad?
- ¿Las dependencias están justificadas?
- ¿Los objetos protegen correctamente sus invariantes?
- ¿Los contenedores de datos se utilizan cuando corresponde?
- ¿Los objetos de valor aportan semántica o seguridad real?
- ¿He evitado clases y abstracciones artificiales?
- ¿Se respetan los principios SOLID cuando son aplicables?
Complejidad y mantenimiento
- ¿La solución es suficientemente sencilla?
- ¿Existe duplicación de reglas de negocio o conocimiento?
- ¿He evitado abstracciones prematuras?
- ¿Los métodos extensos siguen siendo comprensibles?
- ¿La organización facilita localizar y modificar funcionalidades?
- ¿He preservado el comportamiento y los contratos existentes?
- ¿Los cambios están limitados al alcance de la tarea?
Comentarios y formato
- ¿Los comentarios aportan información relevante?
- ¿He eliminado comentarios redundantes u obsoletos?
- ¿El formato es consistente con el proyecto?
- ¿Las operaciones relacionadas están agrupadas?
- ¿He evitado reformatear código ajeno a la modificación?
Principio final
"El código limpio no es el que cumple más reglas, sino el que permite comprender, modificar y verificar el comportamiento con el menor coste razonable."
Utiliza las reglas de esta guía como herramientas para mejorar decisiones de ingeniería.
No sacrifiques la claridad, la simplicidad o la mantenibilidad para cumplir una métrica aislada.
Una solución sencilla, explícita y correctamente verificada es preferible a una arquitectura artificialmente compleja que únicamente aparenta seguir buenas prácticas.