Clean Code: Buenas prácticas que sí funcionan

Ilustración: Clean Code, buenas prácticas de programación que sí funcionan

Clean Code, popularizado por Robert C. Martin, no es un conjunto de reglas rígidas sino una filosofía: el código se escribe una vez pero se lee decenas de veces. En 2026, donde la IA genera código a velocidad industrial, la capacidad de escribir código legible y mantenible es lo que diferencia a un ingeniero de un operador de herramientas.

1. Nombres que revelan intención

El nombre de una variable, función o clase debe responder por qué existe, qué hace y cómo se usa. Si necesita un comentario para explicar un nombre, ese nombre es incorrecto.

// ❌ Mal
const d = new Date();
const arr = users.filter(u => u.s === 1);
function calc(x, y) { return x * y * 0.16; }

// ✅ Bien
const currentDate = new Date();
const activeUsers = users.filter(user => user.status === 'active');
function calculateTaxAmount(price, quantity) {
    const TAX_RATE = 0.16;
    return price * quantity * TAX_RATE;
}

Regla práctica: Si otro desarrollador no entiende qué hace una variable en 3 segundos, renómbrala.

2. Funciones pequeñas con propósito único

Una función debe hacer una sola cosa. Si describes lo que hace usando "y", probablemente hace demasiado.

# ❌ Hace demasiado
def process_user_registration(user_data):
    # Valida
    if not user_data.get('email') or '@' not in user_data['email']:
        return False

    # Encripta
    user_data['password'] = hashlib.sha256(
        user_data['password'].encode()
    ).hexdigest()

    # Guarda en base de datos
    db.users.insert(user_data)

    # Envía email
    smtp.send(user_data['email'], "Bienvenido", "...")

    return True

# ✅ Responsabilidades separadas
def validate_user_data(user_data: dict) -> bool:
    return bool(user_data.get('email')) and '@' in user_data['email']

def hash_password(plain_password: str) -> str:
    return hashlib.sha256(plain_password.encode()).hexdigest()

def save_user(user_data: dict) -> None:
    db.users.insert(user_data)

def send_welcome_email(email: str) -> None:
    smtp.send(email, "Bienvenido", "...")

def register_user(user_data: dict) -> bool:
    if not validate_user_data(user_data):
        return False

    user_data['password'] = hash_password(user_data['password'])
    save_user(user_data)
    send_welcome_email(user_data['email'])
    return True

3. Comentarios: menos es más

El código limpio se documenta a sí mismo. Los comentarios mienten con el tiempo porque el código cambia pero los comentarios no siempre se actualizan.

// ❌ Comentarios que explican lo obvio
// Incrementar el contador en 1
counter++;

// Verificar si el usuario es admin
if (user.role == "admin") { }

// ❌ Comentario como parche a código confuso
// Este método devuelve true si el flag es 2 o 4 pero no 3
public boolean checkStatus(int flag) {
    return flag == 2 || flag == 4;
}

// ✅ Código que se explica solo
public boolean isPaymentApproved(int paymentStatus) {
    return paymentStatus == APPROVED || paymentStatus == PARTIALLY_PAID;
}

// ✅ Comentarios que sí aportan: contexto no obvio de negocio
// PCI-DSS requiere que los últimos 4 dígitos sean los únicos visibles
public String maskCardNumber(String cardNumber) {
    return "**** **** **** " + cardNumber.substring(cardNumber.length() - 4);
}

4. Evitar números y strings mágicos

Los valores literales dispersos en el código son una fuente constante de bugs y confusión.

// ❌ Números mágicos - ¿Qué significan?
if (user.age < 18) { return false; }
if (response.status === 429) { retry(); }
const expires = Date.now() + 86400000;

// ✅ Constantes con nombre
const MINIMUM_AGE_REQUIRED = 18;
const HTTP_STATUS_RATE_LIMITED = 429;
const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;

if (user.age < MINIMUM_AGE_REQUIRED) { return false; }
if (response.status === HTTP_STATUS_RATE_LIMITED) { retry(); }
const expires = Date.now() + ONE_DAY_IN_MILLISECONDS;

5. Manejo de condicionales complejos

Los condicionales anidados son el mayor asesino de legibilidad. Dos patrones resuelven esto efectivamente.

Cláusulas de guarda (Early Return)

// ❌ Pirámide de la perdición
func processOrder(order Order) error {
    if order != nil {
        if order.User != nil {
            if order.User.IsActive {
                if order.Total > 0 {
                    // Lógica real aquí (enterrada)
                    return saveOrder(order)
                }
            }
        }
    }
    return errors.New("invalid order")
}

// ✅ Early return — lógica feliz al final
func processOrder(order Order) error {
    if order == nil {
        return errors.New("order cannot be nil")
    }
    if order.User == nil {
        return errors.New("order must have a user")
    }
    if !order.User.IsActive {
        return errors.New("user account is not active")
    }
    if order.Total <= 0 {
        return errors.New("order total must be positive")
    }

    return saveOrder(order)
}

Encapsular condicionales complejos

// ❌ Condición incomprensible
if (user.age >= 18 && user.verified && !user.banned && user.subscription !== 'expired') {
    grantAccess();
}

// ✅ Condición encapsulada
function canAccessPlatform(user) {
    const isAdult = user.age >= 18;
    const isVerified = user.verified;
    const isNotBanned = !user.banned;
    const hasActiveSubscription = user.subscription !== 'expired';

    return isAdult && isVerified && isNotBanned && hasActiveSubscription;
}

if (canAccessPlatform(user)) {
    grantAccess();
}

6. Estructura y formato consistente

El código debe verse como escrito por una sola persona. La estructura visual impacta directamente en la velocidad de comprensión.

# ❌ Sin estructura ni consistencia
class userService:
    def GetUser(self,id):
        u=db.find(id)
        if u==None:return None
        return u
    def createUser(self,name,email,password):
        if name=='' or email=='' or password=='':return False
        newUser={'name':name,'email':email,'password':password}
        db.insert(newUser)
        return True

# ✅ Estructura clara y consistente
class UserService:

    def get_user(self, user_id: int) -> dict | None:
        return db.find(user_id)

    def create_user(self, name: str, email: str, password: str) -> bool:
        if not all([name, email, password]):
            return False

        user = {
            'name': name,
            'email': email,
            'password': password
        }

        db.insert(user)
        return True

7. No repetir lógica (DRY aplicado correctamente)

La duplicación garantiza inconsistencias. Cuando la lógica cambia, debe cambiar en un solo lugar.

// ❌ Lógica de validación duplicada en múltiples lugares
public void CreateProduct(string name, decimal price) {
    if (string.IsNullOrEmpty(name)) throw new Exception("Name required");
    if (price <= 0) throw new Exception("Invalid price");
    // ...
}

public void UpdateProduct(int id, string name, decimal price) {
    if (string.IsNullOrEmpty(name)) throw new Exception("Name required");
    if (price <= 0) throw new Exception("Invalid price");
    // ...
}

// ✅ Validación centralizada
private void ValidateProduct(string name, decimal price) {
    if (string.IsNullOrEmpty(name))
        throw new ArgumentException("Product name is required");
    if (price <= 0)
        throw new ArgumentException("Price must be greater than zero");
}

public void CreateProduct(string name, decimal price) {
    ValidateProduct(name, price);
    // Lógica de creación
}

public void UpdateProduct(int id, string name, decimal price) {
    ValidateProduct(name, price);
    // Lógica de actualización
}

8. Objetos de datos vs listas de parámetros

Cuando una función recibe más de 3 parámetros, es señal de que esos datos pertenecen juntos en un objeto.

// ❌ Demasiados parámetros
fun sendEmail(
    recipientName: String,
    recipientEmail: String,
    subject: String,
    body: String,
    isHtml: Boolean,
    priority: Int
) { }

// ✅ Objeto de datos cohesivo
data class EmailMessage(
    val recipient: Recipient,
    val subject: String,
    val body: String,
    val isHtml: Boolean = false,
    val priority: Int = NORMAL_PRIORITY
)

data class Recipient(
    val name: String,
    val email: String
)

fun sendEmail(message: EmailMessage) { }

// Uso legible
val message = EmailMessage(
    recipient = Recipient("Juan García", "[email protected]"),
    subject = "Confirmación de pedido",
    body = "

Gracias por tu compra

", isHtml = true ) sendEmail(message)

9. Inmutabilidad por defecto

Prefiera variables inmutables. El estado mutable es la fuente más común de bugs difíciles de rastrear.

// ❌ Estado mutable innecesario
fn calculate_total(items: &Vec<Item>) -> f64 {
    let mut total = 0.0;
    let mut discount = 0.0;
    let mut tax = 0.0;

    for item in items {
        total += item.price;
    }
    discount = total * 0.1;
    tax = (total - discount) * 0.16;

    total - discount + tax
}

// ✅ Transformaciones inmutables
fn calculate_total(items: &[Item]) -> f64 {
    let subtotal: f64 = items.iter().map(|item| item.price).sum();
    let discount = subtotal * 0.10;
    let taxable_amount = subtotal - discount;
    let tax = taxable_amount * 0.16;

    taxable_amount + tax
}

10. Tests como documentación viva

Los tests bien escritos describen el comportamiento esperado mejor que cualquier documentación.

// ❌ Tests que no describen nada
test('test1', () => {
    expect(calc(100, 0.1)).toBe(90);
});

test('discount test', () => {
    expect(calc(100, 0.5)).toBe(50);
});

// ✅ Tests como especificación del sistema
describe('DiscountCalculator', () => {

    describe('cuando el descuento es válido', () => {
        it('aplica porcentaje al precio original', () => {
            const result = applyDiscount(price: 100, discount: 0.10);
            expect(result).toBe(90);
        });

        it('no permite que el precio final sea negativo', () => {
            const result = applyDiscount(price: 100, discount: 1.5);
            expect(result).toBe(0);
        });
    });

    describe('cuando el descuento es inválido', () => {
        it('lanza error con descuento negativo', () => {
            expect(() => applyDiscount(100, -0.1)).toThrow('Discount cannot be negative');
        });
    });
});

El tiempo que tarda en escribir código claro se recupera múltiples veces en debugging reducido, onboarding más rápido de nuevos desarrolladores y menor costo de mantenimiento. El código limpio es la diferencia entre un sistema que escala y uno que se convierte en deuda técnica.

Imagen generada con IA
© Copyright: Natalia Jaimes

Comentarios

Entradas más populares de este blog

vCard vs Linktree ¿Cuál representa mejor tu marca?

3 formas de usar tu vCard en eventos para generar leads reales

El futuro del trabajo: Cómo adaptarse a la automatización y la IA