Ingeniería de Software

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.

15 de enero, 20268 min de lectura
Código LimpioMejores PrácticasRefactoringCalidad de CódigoDiseño de Software

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
    pass

Funciones: 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ía

El 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}")
        raise

Falla 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 abstraer

Optimiza 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:

  1. Los nombres revelan intención - Dedica tiempo a elegir buenos nombres
  2. Las funciones hacen una cosa - Mantenlas pequeñas y enfocadas
  3. Los comentarios explican por qué - No qué hace el código
  4. Los errores son informativos - Ayuda a futuros depuradores
  5. El código se lee de arriba a abajo - Lo importante primero
  6. Consistencia sobre perfección - Sigue las convenciones del equipo
  7. 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

OR

Osvaldo Restrepo

Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.