Principios de Código Limpio Que Realmente Importan
Resumen
Enfócate en legibilidad sobre ingenio, nombres significativos, funciones pequeñas con responsabilidad única y niveles de abstracción apropiados. Evita la optimización prematura y la sobre-ingeniería. El mejor código es código fácil de cambiar.
Después de revisar miles de pull requests y mantener bases de código de todos los tamaños, he identificado qué principios de código limpio realmente mejoran la mantenibilidad y cuáles son cargo cult. Esta guía se enfoca en lo que importa.
La Filosofía Central
El código limpio no se trata de seguir reglas—se trata de comunicación. El código se lee mucho más frecuentemente de lo que se escribe. Martin Fowler (2018) capturó esto: "Cualquier tonto puede escribir código que una computadora entienda. Los buenos programadores escriben código que los humanos pueden entender."
┌─────────────────────────────────────────────────────────────┐
│ La Pirámide del Código Limpio │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ Arq. │ Diseño de sistema │
│ ┌─┴───────┴─┐ │
│ │ Patrones │ Patrones de diseño │
│ ┌─┴───────────┴─┐ │
│ │ Funciones │ Responsabilidad única │
│ ┌─┴───────────────┴─┐ │
│ │ Nombres │ Intención clara │
│ ┌─┴───────────────────┴─┐ │
│ │ Formato │ Consistencia │
│ └─────────────────────────────┘ │
│ │
│ Fundamento → Mayor impacto en la base │
│ │
└─────────────────────────────────────────────────────────────┘
Insight Clave
No puedes tener arquitectura limpia sin funciones limpias, y no puedes tener funciones limpias sin nombres limpios. Construye desde la base hacia arriba.
Nombres: La Fundación
Los Nombres Deben Revelar Intención
# Malo: ¿Qué hace esto?
def calc(a, b, c):
return a * b * (1 - c)
# Bueno: La intención es clara
def calculate_discounted_price(unit_price: float, quantity: int, discount_rate: float) -> float:
return unit_price * quantity * (1 - discount_rate)Usa Lenguaje del Dominio
// Malo: Términos técnicos genéricos
interface DataObject {
id: string;
value: number;
timestamp: Date;
}
// Bueno: Términos específicos del dominio
interface StockPrice {
tickerSymbol: string;
priceInCents: number;
quotedAt: Date;
}Evita Nombres Engañosos
// Malo: accountList es en realidad un Set
Set<Account> accountList = new HashSet<>();
// Malo: hp podría significar cualquier cosa
int hp = 120;
// Bueno: Claro y preciso
Set<Account> activeAccounts = new HashSet<>();
int healthPoints = 120;Longitud Proporcional al Alcance
# Alcance corto = nombre corto está bien
for i in range(10):
print(i)
# Alcance largo = se necesita nombre descriptivo
class OrderProcessor:
def __init__(self):
self.pending_order_count = 0
self.processed_order_count = 0
# El nombre de la función debe describir lo que hace
def u(x): # Malo
pass
def update_user_email_preferences(user: User, preferences: EmailPreferences): # Bueno
passFunciones: Pequeñas y Enfocadas
Responsabilidad Única
# Malo: Hace demasiadas cosas
def process_order(order: Order) -> None:
# Validar
if not order.items:
raise ValueError("Orden vacía")
if not order.customer.address:
raise ValueError("Sin dirección de envío")
# Calcular totales
subtotal = sum(item.price * item.quantity for item in order.items)
tax = subtotal * 0.08
shipping = 5.99 if subtotal < 50 else 0
total = subtotal + tax + shipping
# Actualizar inventario
for item in order.items:
inventory[item.sku] -= item.quantity
# Cobrar pago
payment_gateway.charge(order.customer.payment_method, total)
# Enviar confirmación
email_service.send(order.customer.email, f"Orden confirmada: ${total}")
# Bueno: Responsabilidad única por función
def process_order(order: Order) -> OrderResult:
validate_order(order)
totals = calculate_order_totals(order)
reserve_inventory(order.items)
charge_result = process_payment(order.customer, totals.grand_total)
send_order_confirmation(order, totals)
return OrderResult(order_id=order.id, charged=totals.grand_total)Nivel de Abstracción Apropiado
# Malo: Niveles de abstracción mezclados
def generate_report(data: list[dict]) -> str:
# Alto nivel
report = Report()
# De repente bajo nivel
html = "<html><head><style>"
html += "table { border-collapse: collapse; }"
html += "</style></head><body>"
# De vuelta a alto nivel
report.add_summary(data)
# Bajo nivel de nuevo
for row in data:
html += f"<tr><td>{row['name']}</td><td>{row['value']}</td></tr>"
# Bueno: Abstracción consistente
def generate_report(data: list[dict]) -> str:
report = Report()
report.add_header()
report.add_summary(calculate_summary(data))
report.add_data_table(data)
report.add_footer()
return report.render()Minimiza Parámetros
// Malo: Demasiados parámetros
function createUser(
firstName: string,
lastName: string,
email: string,
phone: string,
address: string,
city: string,
country: string,
preferences: string[]
): User {
// ...
}
// Bueno: Usa un objeto
interface CreateUserRequest {
name: { first: string; last: string };
contact: { email: string; phone?: string };
address: { street: string; city: string; country: string };
preferences?: string[];
}
function createUser(request: CreateUserRequest): User {
// ...
}Error Común
No extraigas ciegamente cada pocas líneas en una función. Las funciones deben representar unidades de trabajo significativas, no conteos arbitrarios de líneas. Una función de 20 líneas que hace una cosa clara es mejor que 5 funciones que fragmentan la lógica.
Comentarios: Cuándo y Cómo
Buenos Comentarios
# Explica POR QUÉ, no QUÉ
# Usamos un timeout de 3 segundos porque el servicio upstream
# tiene picos de latencia conocidos durante horas pico (ver incidente #1234)
response = client.fetch(url, timeout=3.0)
# Clarifica reglas de negocio complejas
# La exención de impuestos aplica a organizaciones sin fines de lucro (501c3)
# registradas antes de 2020, según la resolución del IRS 2019-42
if org.is_nonprofit and org.registration_year < 2020:
tax_rate = 0
# Advierte sobre consecuencias
# ADVERTENCIA: Este caché es compartido entre todas las instancias.
# Limpiarlo causará un problema de thundering herd.
# Usa el método gradual_clear() en su lugar.
cache.clear()Malos Comentarios
// Malo: Dice lo obvio
i++; // Incrementar i
// Malo: Comentario desactualizado (el código cambió, el comentario no)
// Calcular el promedio de tres números
return (a + b) / 2;
// Malo: Código comentado
// public void oldMethod() {
// // Esta era la implementación vieja
// doOldThing();
// }
// Malo: Comentarios de diario (para eso está git)
// Modificado por Juan el 2024-01-15
// Corregido bug reportado por MaríaEl Mejor Comentario es Ningún Comentario
# En lugar de esto:
# Verificar si el usuario es elegible para funciones premium
if user.subscription_level >= 2 and user.account_age_days > 30 and not user.is_suspended:
enable_premium()
# Escribe código auto-documentado:
def is_eligible_for_premium(user: User) -> bool:
has_premium_subscription = user.subscription_level >= PREMIUM_TIER
is_established_account = user.account_age_days > MINIMUM_ACCOUNT_AGE
is_in_good_standing = not user.is_suspended
return has_premium_subscription and is_established_account and is_in_good_standing
if is_eligible_for_premium(user):
enable_premium()Manejo de Errores
Usa Excepciones para Casos Excepcionales
# Malo: Usar excepciones para flujo de control
def find_user(user_id: str) -> User:
try:
return database.get(user_id)
except:
return None # Traga todos los errores
# Bueno: Manejo de errores claro
def find_user(user_id: str) -> User | None:
"""Retorna None si no se encuentra el usuario, lanza excepción en errores de base de datos."""
try:
return database.get(user_id)
except UserNotFoundError:
return None
except DatabaseConnectionError:
logger.error(f"Error de base de datos buscando usuario {user_id}")
raiseFalla Rápido
// Malo: Continuar con datos malos
function processPayment(amount: number, currency: string): PaymentResult {
// Procesa incluso con entrada inválida, causando bugs raros después
const convertedAmount = amount || 0;
// ...
}
// Bueno: Valida temprano
function processPayment(amount: number, currency: string): PaymentResult {
if (amount <= 0) {
throw new InvalidAmountError(`El monto debe ser positivo, recibido: ${amount}`);
}
if (!SUPPORTED_CURRENCIES.includes(currency)) {
throw new UnsupportedCurrencyError(`Moneda no soportada: ${currency}`);
}
// Ahora sabemos que tenemos datos válidos
// ...
}Proporciona Contexto
# Malo: Error genérico
raise Exception("Falló")
# Bueno: Mensaje de error accionable
raise PaymentProcessingError(
f"Falló al cobrar tarjeta terminada en {card_last_four} por ${amount:.2f}. "
f"Respuesta del gateway: {gateway_response.error_code} - {gateway_response.message}. "
f"ID de transacción: {transaction_id}"
)Limpieza Pragmática
No Sobre-Ingenies
# Sobre-ingeniería: Factory para una sola implementación
class DatabaseConnectionFactory:
def create_connection(self, config: Config) -> DatabaseConnection:
return PostgresConnection(config)
# Solo usa la cosa directamente
connection = PostgresConnection(config)Tolera Algo de Duplicación
# No abstraigas muy temprano
# Si tienes dos funciones similares, frecuentemente está bien
def create_admin_user(name: str, email: str) -> User:
user = User(name=name, email=email, role="admin")
user.permissions = ADMIN_PERMISSIONS
notify_admin_created(user)
return user
def create_regular_user(name: str, email: str) -> User:
user = User(name=name, email=email, role="user")
user.permissions = DEFAULT_PERMISSIONS
send_welcome_email(user)
return user
# La abstracción prematura hace el código más difícil de entender
# Espera hasta tener tres+ casos antes de abstraerOptimiza para Lectura, No Escritura
// One-liner ingenioso (difícil de leer)
const result = data.filter(x => x.active).reduce((a, b) => ({...a, [b.id]: b.items.flatMap(i => i.tags).filter((t, i, arr) => arr.indexOf(t) === i)}), {});
// Versión multi-línea clara (fácil de leer)
const activeItems = data.filter(item => item.active);
const result: Record<string, string[]> = {};
for (const item of activeItems) {
const allTags = item.items.flatMap(i => i.tags);
const uniqueTags = [...new Set(allTags)];
result[item.id] = uniqueTags;
}Conclusión
Principios de código limpio que consistentemente importan:
- Los nombres revelan intención - Dedica tiempo a elegir buenos nombres
- Las funciones hacen una cosa - Mantenlas pequeñas y enfocadas
- Los comentarios explican por qué - No qué hace el código
- Los errores son informativos - Ayuda a futuros depuradores
- El código se lee de arriba a abajo - Lo importante primero
- Consistencia sobre perfección - Sigue las convenciones del equipo
- Simplicidad sobre ingenio - El código aburrido es buen código
El objetivo no es código perfecto—es código fácil de cambiar cuando los requisitos cambien. Y siempre cambian.
Referencias
Martin, R. C. (2008). Clean code: A handbook of agile software craftsmanship. Prentice Hall.
Fowler, M. (2018). Refactoring: Improving the design of existing code (2nd ed.). Addison-Wesley.
Thomas, D., & Hunt, A. (2019). The pragmatic programmer: Your journey to mastery (20th anniversary ed.). Addison-Wesley.
McConnell, S. (2004). Code complete: A practical handbook of software construction (2nd ed.). Microsoft Press.
¿Quieres mejorar la calidad de código de tu equipo? Contáctame para discutir prácticas de revisión de código y estándares.
Frequently Asked Questions
Osvaldo Restrepo
Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.