Clean Code: Buenas prácticas 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.
© Copyright: Natalia Jaimes
Comentarios
Publicar un comentario