13 · Detectar olores y refactorizar con evidencia#
Un code smell (olor de código) no prueba que el programa esté equivocado ni que contenga un fallo de ejecución inmediato: es un síntoma superficial en la estructura del código que sugiere una debilidad más profunda y anticipa un aumento en el costo de mantenimiento futuro. Esta lección establece una disciplina rigurosa para separar tres conceptos que los desarrolladores suelen confundir: un fallo funcional observable (bug), una estructura incómoda o acoplada (smell) y un cuello de botella de rendimiento no medido. Aprenderás a formular hipótesis basadas en evidencia, a blindar el comportamiento con pruebas automatizadas y a refactorizar paso a paso preservando el contrato externo del sistema.
Objetivo#
Al terminar esta lección podrás:
- Identificar y nombrar olores de código clásicos (Primitive Obsession, Flag Argument, Shotgun Surgery, Divergent Change, Accidental Duplication) mediante observaciones objetivas y reproducibles en el código fuente.
- Distinguir formalmente entre corregir un fallo funcional, refactorizar una estructura interna y optimizar el rendimiento bajo métricas de profiling.
- Establecer una línea base en verde (GREEN baseline) antes de alterar cualquier estructura interna, garantizando la preservación estricta del contrato observable.
- Reemplazar argumentos booleanos ambiguos (boolean blindness) mediante tipos algebraicos expresivos en Rust (
enum), logrando sitios de llamada autoexplicativos que impiden combinaciones inválidas en tiempo de compilación. - Justificar técnicamente cuándo NO debe realizarse una refactorización para evitar el sobrecoste de abstracciones prematuras o el acoplamiento derivado de deduplicaciones puramente sintácticas.
Antes de empezar#
Para aprovechar al máximo esta lección, necesitas familiaridad con funciones, enumeraciones, coincidencia de patrones (match), tipos de error (Result) y la suite de pruebas estándar de Rust (cargo test o rustc --test).
En esta lección utilizaremos exclusivamente la biblioteca estándar de Rust (std-only). Cada fragmento de código autónomo está diseñado para ser verificado directamente en tu terminal:
rustc --test smell.rs
Ten presente la distinción epistemológica fundamental antes de modificar una sola línea:
- *Corregir un defecto (Bug fixing):* El comportamiento observable actual es incorrecto según la especificación del dominio; la meta es alterar la salida observada para que coincida con la regla esperada.
- *Refactorizar (Refactoring):* El comportamiento observable actual es considerado válido y se mantiene intacto; la meta es reestructurar el diseño interno para reducir la fricción de cambio, mejorar la legibilidad y elevar la cohesión.
- *Optimizar (Optimization):* La estructura o los algoritmos se ajustan para reducir el consumo medido de un recurso físico (tiempo de CPU, memoria RAM, operaciones de I/O); la meta es alterar las características no funcionales sin modificar el resultado lógico.
Si encuentras términos desconocidos durante la lectura, consulta el glosario técnico.
Diccionario de conceptos de la lección (Glosario desde cero)#
Para adquirir un criterio de diseño sólido, analizamos los conceptos centrales de la refactorización bajo cuatro dimensiones: intuición cotidiana, definición técnica formal, ejemplo concreto en código y concepción errónea frecuente.
1. Olor de código (Code Smell, definición de Fowler/Beck)#
- Intuición cotidiana: Un olor agrio en la leche no significa necesariamente que ya esté cuajada o que te vaya a enfermar al instante, pero es una advertencia química inconfundible de que las bacterias se están multiplicando y pronto se agriará por completo.
- Definición técnica precisa: Término acuñado por Kent Beck y popularizado por Martin Fowler. Es un síntoma observable en el código fuente (como una clase con demasiadas líneas, métodos con listas interminables de parámetros o condicionales anidados recurrentes) que, sin ser un error de sintaxis ni provocar un fallo funcional en tiempo de ejecución, viola principios de diseño modular y señala una alta probabilidad de degradación estructural y fallos futuros.
- Ejemplo en código:
// Smell: Boolean Blindness / Flag Argument
// ¿Qué significa 'true' en el punto de llamada?
let fee = calculate_shipping(1500, true);
- Concepción errónea: Creer que un code smell es un bug. Un programa repleto de olores de código puede compilar perfectamente, pasar todas las pruebas de negocio y procesar millones de transacciones al día sin fallar; el impacto de los olores no es el fallo del software, sino la lentitud y el peligro que experimentan los ingenieros al intentar modificarlo.
2. Refactorización (Refactoring)#
- Intuición cotidiana: Reorganizar las tuberías y el cableado eléctrico detrás de las paredes de una cocina sin cambiar la posición de los grifos ni el voltaje de los enchufes: el usuario abre el grifo y obtiene exactamente la misma agua a la misma presión, pero ahora el fontanero puede cambiar una válvula en 5 minutos sin romper la pared entera.
- Definición técnica precisa: Transformación disciplinada y controlada de la estructura interna de un sistema de software que mejora su inteligibilidad, reduce su complejidad ciclomática y abarata futuras extensiones, sin alterar en absoluto su comportamiento observable externo (preserva de forma estricta las precondiciones, poscondiciones e invariantes del contrato público).
- Ejemplo en código:
// Before: duplicated logic in two distinct functions
fn is_adult_driver(age: u32) -> bool { age >= 18 }
fn is_adult_voter(age: u32) -> bool { age >= 18 }
// After refactoring: extracted single domain rule
const LEGAL_ADULT_AGE: u32 = 18;
fn is_legal_adult(age: u32) -> bool { age >= LEGAL_ADULT_AGE }
- Concepción errónea: Llamar "refactorización" a reescribir un módulo desde cero, cambiar el formato de las respuestas JSON de un endpoint o alterar las reglas de negocio al mismo tiempo que se limpia el código. Si el comportamiento observable cambia, se trata de un rediseño funcional o corrección de defectos, no de una refactorización.
3. Cohesión vs Acoplamiento (Cohesion vs Coupling)#
- Intuición cotidiana:
- Alta cohesión: Una navaja suiza donde cada accesorio está diseñado específicamente para cortar o desarmar cosas manuales pequeñas.
- Bajo acoplamiento: Conectar tu teléfono a un cargador mediante un cable USB-C estándar: el teléfono no necesita saber si la electricidad proviene de una presa hidroeléctrica, un panel solar o un generador diésel.
- Definición técnica precisa:
- Cohesión: Medida del grado en que las responsabilidades y operaciones internas de un módulo, función o tipo de dato pertenecen juntas desde el punto de vista conceptual. Un módulo altamente cohesivo realiza una única tarea bien delimitada sobre un conjunto uniforme de datos.
- Acoplamiento: Grado de interdependencia entre dos o más módulos de software. Un acoplamiento estrecho (tight coupling) implica que un cambio en los detalles internos de implementación de un módulo obliga a realizar cambios en cascada en los módulos que lo consumen.
- Ejemplo en código:
// High cohesion, low coupling:
// The function only calculates discounts; it does not read DB or format HTML
fn calculate_discount(cents: u32, percent: u32) -> u32 {
(cents * percent) / 100
}
- Concepción errónea: Pensar que la cohesión se mide por la cantidad de líneas de código. Una función de 15 líneas que valida un token JWT, escribe en PostgreSQL y serializa una respuesta JSON tiene una cohesión pésima (baja), mientras que una función algorítmica de 70 líneas que realiza un ordenamiento topológico puro posee una cohesión excelente (alta).
4. Argumento bandera (Flag Argument / Boolean Blindness)#
- Intuición cotidiana: Presionar un interruptor que hace que una tostadora se convierta de pronto en una licuadora. El usuario tiene un único botón, pero el aparato realiza dos tareas ontológicamente dispares según la posición del switch.
- Definición técnica precisa: Antipatrón donde se pasa un parámetro de control (comúnmente un booleano
boolo una enumeración genérica) a una función para indicarle internamente qué rama de ejecución o qué operación conceptual ejecutar. En los sitios de llamada, este patrón provoca el fenómeno conocido en teoría de tipos como Boolean Blindness (ceguera booleana): el lector del código no puede deducir la semántica detrueofalsesin inspeccionar la implementación interna de la función llamada. - Ejemplo en código:
// Flag argument: what does 'false' mean at the call site?
// Does it mean "do not notify", "dry run", or "synchronous mode"?
process_order(order_id, false);
- Concepción errónea: Creer que añadir un booleano es la forma más "barata" y rápida de extender una función existente cuando surge un nuevo requisito. Aunque evita crear una nueva función en el corto plazo, duplica la complejidad ciclomática de la función existente y vuelve ilegibles todos los puntos de llamada del sistema.
5. Obsesión por primitivos (Primitive Obsession) y patrón Newtype#
- Intuición cotidiana: Utilizar trozos de cuerda común de ferretería para conectar cualquier cosa en tu casa: para atar una caja, amarrar la manguera del gas y sujetar los cables de alta tensión. Una cuerda sirve para atar, pero carece de las propiedades de aislamiento eléctrico o resistencia química que cada uso exige.
- Definición técnica precisa: Uso indiscriminado de tipos de datos primitivos provistos por el lenguaje (
u32,i64,String,bool) para representar entidades y conceptos del dominio que poseen restricciones, invariantes y reglas de negocio propias (como direcciones de correo electrónico, números de teléfono, identificadores de usuario o montos monetarios). En Rust, este problema se resuelve idiomáticamente mediante el patrón Newtype (struct Email(String); struct OrderId(u64);), el cual encapsula el primitivo en una estructura de tupla de costo cero en tiempo de compilación (zero-cost abstraction). - Ejemplo en código:
// Primitive obsession:
fn transfer(from_account: u64, to_account: u64, amount: u64) { /* ... */ }
// Danger: transfer(100, 200, 500) can be accidentally flipped to transfer(500, 200, 100)
// Newtype pattern in Rust:
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct AccountId(pub u64);
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Cents(pub u64);
fn safe_transfer(from: AccountId, to: AccountId, amount: Cents) { /* ... */ }
- Concepción errónea: Pensar que envolver un número primitivo en un struct Newtype en Rust añade sobrecoste de memoria o ralentiza las llamadas a funciones. El compilador de Rust desmantela (unwraps) la estructura Newtype durante la optimización LLVM; en el binario final generado, un
AccountIdocupa exactamente el mismo espacio y utiliza las mismas instrucciones de registro que unu64crudo.
6. Cirugía de escopeta (Shotgun Surgery) vs Cambio divergente (Divergent Change)#
- Intuición cotidiana:
- Cirugía de escopeta: Disparar un cartucho de perdigones a una pared: un solo disparo impacta y perfora decenas de lugares dispersos a la vez.
- Cambio divergente: Un único cajón de cocina donde guardas cuchillos, medicinas, facturas de impuestos y ropa deportiva: tienes que abrir y revolver ese mismo cajón por motivos completamente ajenos entre sí.
- Definición técnica precisa:
- *Cirugía de escopeta (Shotgun Surgery):* Olor estructural en el que introducir un único cambio conceptual en las reglas de negocio obliga a realizar pequeñas modificaciones en múltiples archivos y módulos distribuidos a lo largo de todo el proyecto. Indica que una responsabilidad de dominio está fragmentada y dispersa sin un centro de gravedad claro.
- *Cambio divergente (Divergent Change): Olor en el que un único módulo o archivo debe ser modificado continuamente por razones completamente distintas y desconectadas (por ejemplo, cambiar la regla de cálculo de impuestos, cambiar la biblioteca de serialización JSON y cambiar las credenciales de conexión a la base de datos dentro del mismo archivo). Viola directamente el Principio de Responsabilidad Única (Single Responsibility Principle*).
- Ejemplo en código:
- Shotgun Surgery: Cuando agregar un nuevo estado de pedido (
Cancelled) requiere editar el enum enorder.rs, unmatchenbilling.rs, una consulta SQL enrepository.rs, una vista enui.rsy un mapeador enwebhook.rs. - Concepción errónea: Pensar que ambos términos son sinónimos. Son problemas inversos: Divergent Change es un archivo que cambia por muchos motivos (falta de separación interna), mientras que Shotgun Surgery es un motivo de cambio que dispersa ediciones en muchos archivos (falta de encapsulamiento unificado).
7. Duplicación semántica vs Duplicación sintáctica accidental#
- Intuición cotidiana:
- Duplicación semántica: Dos empleados en dos departamentos distintos tienen fotocopiada la misma ley laboral oficial. Si la ley cambia en el Congreso, ambas fotocopias deben actualizarse exactamente igual porque representan el mismo mandato legal.
- Duplicación accidental: Dos personas en la oficina usan por casualidad la misma contraseña de cuatro dígitos
1234. No comparten ninguna relación; si una persona decide cambiar su clave bancaria, la otra no tiene por qué cambiar la suya. Unirlas sería forzar una dependencia inexistente. - Definición técnica precisa:
- Duplicación semántica (conascencia de significado): Dos o más fragmentos de código representan la misma regla de negocio o invariante ontológica. Si la regla de dominio evoluciona, ambos fragmentos deben cambiar obligatoriamente en sincronía. Debe ser abstraída a un único punto de verdad.
- Duplicación sintáctica accidental: Dos fragmentos de código poseen una estructura de caracteres o instrucciones similar o idéntica en el presente, pero representan conceptos de dominio independientes con ciclos de vida y motivos de cambio totalmente divergentes. Unificarlas mediante una función común introduce un acoplamiento artificial y tóxico.
- Ejemplo en código:
// Task title validation in web API: cannot exceed 100 characters
fn validate_task_title(t: &str) -> bool { t.len() <= 100 }
// Chat message validation in support chat: cannot exceed 100 characters
fn validate_chat_message(m: &str) -> bool { m.len() <= 100 }
- Concepción errónea: Seguir el principio DRY (Don't Repeat Yourself) de forma dogmática y miope, deduplicando cualquier bloque de código de más de tres líneas sin preguntarse si los conceptos comparten la misma razón de cambio. La duplicación accidental es mucho más barata de mantener que una abstracción equivocada (wrong abstraction).
8. Contrato de comportamiento y Línea base en verde (GREEN baseline)#
- Intuición cotidiana: Tomar una fotografía panorámica de alta resolución de una habitación antes de comenzar una mudanza: si al terminar de mover los muebles una alfombra o un cuadro desaparece, la fotografía prueba de inmediato que la mudanza alteró algo que debía permanecer intacto.
- Definición técnica precisa:
- Contrato de comportamiento: El conjunto explícito de precondiciones (entradas válidas e inválidas), poscondiciones (salidas calculadas, errores devueltos) e invariantes observables que un componente garantiza a sus clientes externos.
- *Línea base en verde (GREEN baseline): Una suite automatizada de pruebas unitarias y de integración que cubre exhaustivamente el comportamiento existente del componente y cuya ejecución reporta un éxito rotundo (100% pruebas pasando, estado GREEN) antes* de que el programador realice la menor alteración estructural en el código de producción. Actúa como arnés de seguridad contra regresiones.
- Ejemplo en código:
#[test]
fn baseline_shipping_contract() {
// Verifies existing behavior before modifying production code
assert_eq!(calculate_shipping(0, false), 250);
assert_eq!(calculate_shipping(1000, false), 0);
}
- Concepción errónea: Iniciar una refactorización con pruebas que ya están fallando (estado RED) creyendo que "se arreglarán sobre la marcha". Si comienzas a cambiar la estructura mientras hay pruebas en rojo, pierdes la capacidad de discernir si un fallo posterior fue causado por el defecto original o por un error introducido durante la refactorización.
9. Cuello de botella de rendimiento y medición reproducible (Profiling/Benchmarking)#
- Intuición cotidiana: Una carretera de tres carriles que converge en un puente angosto de una sola vía: pavimentar con asfalto de carreras los kilómetros previos al puente no reduce en absoluto el tiempo total del atasco; la congestión solo se resuelve ensanchando el puente.
- Definición técnica precisa:
- *Cuello de botella (Bottleneck): El componente, instrucción, contención de bloqueo (lock contention) o latencia de I/O que limita la capacidad de procesamiento (throughput*) o la latencia global de un sistema.
- *Medición reproducible (Benchmarking/Profiling):* Registro cuantitativo de métricas de rendimiento bajo condiciones ambientales controladas y repetibles, utilizando herramientas especializadas (
perf,flamegraph,criterion) para obtener evidencia empírica irrefutable antes de justificar cualquier cambio en el código. - Ejemplo en código:
// Naive intuition: "replacing a loop with an iterator speeds up execution 10x"
// Technical reality: in Rust, the LLVM optimizer often compiles both approaches
// to the exact same vectorized AVX2 machine instructions.
- Concepción errónea: Confundir contar líneas de código o complejidad visual con consumo de tiempo de CPU. Afirmar que un bloque de código "es lento" basándose en la intuición personal sin presentar un informe de profiling reproducible es una forma de optimización prematura, la cual con frecuencia deteriora la legibilidad del sistema a cambio de cero nanosegundos de mejora real.
Diagrama#
El siguiente diagrama formaliza el flujo de decisión disciplinado entre la aparición de un síntoma en el código y la acción técnica justificada:
flowchart TD
A[Observar fricción reproducible] --> B{¿Hay comportamiento incorrecto?}
B -->|Sí| C[Proteger y corregir el bug]
B -->|No| D[Formular hipótesis de estructura]
D --> E[Escribir prueba de comportamiento]
E --> F[Cambio mínimo]
F --> G[Comparar evidencia y costo]
G -->|No mejoró| H[Revertir o reformular]
G -->|Mejoró sin cambiar contrato| I[Conservar]
Modelo mental y desempacado paso a paso#
1. La disciplina de los dos sombreros: por qué la línea base en verde es innegociable#
En su obra fundacional Refactoring: Improving the Design of Existing Code, Kent Beck y Martin Fowler formularon la metáfora de los dos sombreros (Two Hats). Al desarrollar software, un ingeniero debe alternar conscientemente entre dos modos de trabajo estrictamente disjuntos, pero jamás usarlos al mismo tiempo:
- El sombrero de funcionalidad / corrección de fallos: Buscas alterar el comportamiento observable del sistema. Añades un nuevo endpoint, cambias una regla impositiva o corriges un cálculo erróneo. Escribes pruebas que fallan (fase RED) y añades código para hacerlas pasar (fase GREEN). El contrato del sistema cambia.
- El sombrero de refactorización: Buscas reestructurar el sistema sin alterar su comportamiento observable. No agregas funcionalidades ni arreglas bugs que descubras en el camino. Solo mueves código, extraes funciones, renombras tipos o desacoplas módulos. Las pruebas existentes deben permanecer rigurosamente en verde durante el 100% del proceso.
Si intentas cambiar la estructura interna al mismo tiempo que corriges un bug o añades un requisito nuevo, estás realizando una reescritura descontrolada. Si el sistema falla, ¿se debe a la nueva regla o a la extracción de la función? Sin una línea base en verde (GREEN baseline), el desarrollador queda atrapado en un laberinto de diagnósticos confusos.
El protocolo riguroso ante un código heredado con defectos es secuencial:
- Escribe una prueba automatizada que reproduzca y confirme el fallo actual (la prueba falla: fase RED).
- Corrige el defecto con la mínima intervención posible hasta que la prueba pase (fase GREEN).
- Ahora que posees una suite completa en verde, ponte el sombrero de refactorización. Reestructura el código limpio y comprueba a cada paso que la suite continúe en verde.
2. Cohesión vs longitud: la falacia de contar líneas de código#
Existe un mito extendido según el cual "las funciones cortas son siempre limpias y las funciones largas son siempre olores de código". Esta heurística simplista es peligrosa.
Considera esta función de apenas 12 líneas:
// Illustrative example of low cohesion in a short function
fn handle_user_request(token: &str, user_id: u64, amount: u32) -> Result<String, String> {
if !token.starts_with("Bearer ") { return Err("Unauthorized".into()); }
let query = format!("UPDATE accounts SET balance = balance + {amount} WHERE id = {user_id};");
println!("EXECUTING SQL: {query}");
Ok(format!("{{\"status\":\"success\",\"credited\":{amount}}}"))
}
A pesar de su brevedad, esta función tiene una cohesión desastrosa:
- Mezcla autorización y parsing de cabeceras HTTP.
- Construye consultas SQL crudas (abriendo una vulnerabilidad de inyección SQL catastrófica).
- Realiza efectos secundarios de registro en consola (logging).
- Serializa cadenas de texto en formato JSON.
Cualquier cambio en la política de autenticación, en el esquema de la base de datos o en el contrato de respuesta romperá esta función.
Por el contrario, una función pura de 60 líneas que implementa el algoritmo de descompresión Huffman o el cálculo de rutas Dijkstra es altamente cohesiva: todas sus instrucciones operan sobre las mismas estructuras de datos en memoria para resolver un único problema matemático bien delimitado.
El veneno de la deduplicación accidental#
Deduplicar código únicamente porque dos bloques comparten la misma sintaxis textual es una de las causas principales de degradación arquitectónica:
// Two business rules that currently look identical purely by coincidence:
fn validate_checkout_age(age: u32) -> bool { age >= 18 }
fn validate_film_rating_age(age: u32) -> bool { age >= 18 }
Si un programador novato extrae esto a validate_age(age: u32) -> bool y la consume desde el módulo de pagos y desde el catálogo de películas, ha generado un acoplamiento patológico. Si el gobierno modifica la edad legal de comercio electrónico a 21 años mientras que la clasificación de películas permanece en 18, el desarrollador que intente actualizar la función validate_age romperá involuntariamente la política de alquiler de películas, o se verá forzado a introducir un argumento bandera booleano (validate_age(age, for_checkout)), empeorando aún más el diseño.
3. Ceguera booleana (Boolean Blindness) y tipos algebraicos en Rust#
El argumento bandera (Flag Argument) degrada la cognición del desarrollador mediante la ceguera booleana:
// What does 'true' indicate at the call site?
let total = calculate_fee(4500, true);
Al leer la llamada, el desarrollador no sabe si true significa:
- ¿Es recogida en tienda (pickup)?
- ¿Es cliente VIP?
- ¿Incluye impuestos?
- ¿Aplica recargo nocturno?
Además, los booleanos no escalan de forma ortogonal. Si el negocio luego introduce "Envío exprés 24h", el programador suele caer en la tentación de agregar un segundo booleano:
let total = calculate_fee(4500, false, true); // What do these positional booleans mean?
Esto genera 2² = 4 estados posibles, incluyendo combinaciones incoherentes o ilegales: ¿qué ocurre si el llamador pasa calculate_fee(4500, true, true) (recogida en tienda y al mismo tiempo envío exprés)? La función debe añadir lógica defensiva interna para detectar y rechazar combinaciones absurdas que el propio sistema de tipos debió haber impedido.
En Rust, la solución idiomática es utilizar un tipo suma algebraico (enum):
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeliveryMode {
Pickup,
StandardDelivery,
ExpressDelivery,
}
pub fn calculate_fee(subtotal_cents: u32, mode: DeliveryMode) -> u32 {
match mode {
DeliveryMode::Pickup => 0,
DeliveryMode::StandardDelivery => if subtotal_cents >= 1000 { 0 } else { 250 },
DeliveryMode::ExpressDelivery => 500,
}
}
Beneficios inmediatos:
- Autoexplicativo: En el punto de llamada se escribe
calculate_fee(4500, DeliveryMode::Pickup). Cero ambigüedad para quien lee o revisa el código. - Imposibilidad de estados ilegales: Es ontológicamente imposible pasar
PickupyExpressDeliverya la vez; el tipo suma garantiza que el valor es estrictamente una de las variantes válidas. - Chequeo de exhaustividad en el compilador: Si en el futuro se añade una variante
LockerDelivery, el compilador de Rust exigirá revisar todos losmatchdel sistema que consuman este enum, previniendo ramas olvidadas.
Ejemplo razonado#
El siguiente bloque es un programa Rust completo ejecutable directamente con rustc --test smell.rs. Ilustra el proceso de refactorización de una regla de validación de cadenas compartida entre un formulario web y un proceso de importación masiva.
// Domain: Title validation rule
// Contract: A title is valid if and only if it contains non-whitespace characters.
// Cohesive pure function encapsulating the domain policy
pub fn is_valid_title(title: &str) -> bool {
!title.trim().is_empty()
}
// User interface adapter (form input)
pub fn form_accepts(title: &str) -> bool {
is_valid_title(title)
}
// Batch ingestion adapter (importer)
pub fn import_accepts(title: &str) -> bool {
is_valid_title(title)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn baseline_accepts_valid_titles() {
assert!(is_valid_title("Rust Design Patterns"));
assert!(form_accepts("Rust Design Patterns"));
assert!(import_accepts("Rust Design Patterns"));
}
#[test]
fn baseline_rejects_empty_or_whitespace_strings() {
assert!(!is_valid_title(""));
assert!(!is_valid_title(" \t\n "));
assert!(!form_accepts(" "));
assert!(!import_accepts(" "));
}
#[test]
fn boundary_case_single_character() {
assert!(is_valid_title("A"));
assert!(!is_valid_title(" "));
}
}
Análisis del cambio:#
- Línea base previa: Antes de unificar la lógica, se comprobó que tanto el formulario como el importador debían rechazar exactamente las cadenas vacías o con puros espacios en blanco.
- Extracción justificada: En este caso, la regla "qué constituye un título aceptable en el sistema" es una invariante ontológica del negocio, no una coincidencia visual. Unificarla en
is_valid_titlegarantiza que el importador y el formulario no diverjan con el tiempo. - Pruebas de preservación: Las pruebas
baseline_accepts_valid_titlesybaseline_rejects_empty_or_whitespace_stringsse ejecutaron antes y después de la extracción; en ambas fases el resultado fue idéntico, demostrando que la refactorización preservó el contrato en su totalidad.
Práctica independiente#
En este ejercicio implementarás una función de cálculo de tarifas de envío y eliminarás un olor de argumento bandera mediante una refactorización guiada por pruebas.
1. Especificación del dominio#
Una tienda en línea aplica las siguientes políticas para calcular el costo de envío de una orden:
- Moneda: Todos los cálculos se realizan en centavos enteros (
u32) para garantizar precisión estricta. - Tarifa estándar: $250$ centavos ($2.50 dólares).
- Umbral de envío gratuito: Si el subtotal de la compra alcanza o supera los $1000$ centavos (10.00 dólares), el envío estándar es gratuito (0$ centavos).
- *Modalidad de recogida local (Pickup):* Si el cliente recoge el pedido en la tienda física, el costo de envío es siempre $0$ centavos, independientemente del monto del subtotal.
2. Matriz de partición de equivalencias y casos límite (Boundary Values)#
Antes de escribir una sola línea de lógica interna, completa y predice la salida exacta de cada combinación en la siguiente matriz de prueba:
| Identificador | Subtotal (cents) | Modalidad de entrega | Tarifa esperada (cents) | Razón de dominio / Frontera |
|---|---|---|---|---|
| TC-01 | 0 | Recogida local (Pickup) | 0 | Orden sin costo en mostrador |
| TC-02 | 0 | Envío estándar (Standard) | 250 | Orden mínima por debajo del umbral |
| TC-03 | 999 | Envío estándar (Standard) | 250 | Justo un centavo por debajo del umbral |
| TC-04 | 1000 | Envío estándar (Standard) | 0 | Exactamente en el umbral de gratuidad |
| TC-05 | 1001 | Envío estándar (Standard) | 0 | Justo por encima del umbral de gratuidad |
| TC-06 | 1000 | Recogida local (Pickup) | 0 | Recogida en el umbral (doble condición de gratuidad) |
3. Lista de verificación de refactorización paso a paso (TDD)#
Sigue rigurosamente estos pasos:
- Fase RED (Línea base fallida):
- Modela la función inicial con el olor típico de argumento bandera:
pub fn shipping_fee_legacy(subtotal: u32, is_pickup: bool) -> u32. - Haz que devuelva un valor deliberadamente erróneo (como9999). - Escribe las pruebas unitarias basadas en la matriz anterior. - Ejecutarustc --test shipping.rsy observa cómo las pruebas fallan reportando la causa exacta. - Fase GREEN (Línea base satisfecha):
- Implementa la lógica mínima con condicionales para hacer pasar las 6 pruebas.
- Ejecuta las pruebas y confirma el estado:
test result: ok. 6 passed. Esta es tu línea base en verde. - Fase REFACTOR (Mejora estructural sin alterar contrato):
- Introduce el tipo enumerado autoexplicativo:
``
rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeliveryMode { Pickup, StandardDelivery, }`- Escribe la función refactorizada:pub fn shipping_fee(subtotal_cents: u32, mode: DeliveryMode) -> u32. - Reemplaza los condicionales crudos con unmatchexhaustivo sobremodey constantes bien nombradas (FREE_SHIPPING_THRESHOLD_CENTS,STANDARD_FEE_CENTS). - Adapta o añade pruebas equivalentes con el enum y confirma que todas las aserciones sigan pasando. - Compara la legibilidad en el punto de llamada: contrastashipping_fee_legacy(0, true)conshipping_fee(0, DeliveryMode::Pickup)`.
Consulta la guía de soluciones en ../solutions/04-design.md tras completar tu intento independiente.
Preguntas de comprensión profunda y diagnóstico socrático#
Reflexiona sobre estas cinco preguntas de diseño. Cada una presenta un dilema arquitectónico real que exige contrastar compensaciones (trade-offs) y razonar desde primeros principios:
Pregunta 1: La regla de los dos sombreros y el peligro de refactorizar en rojo#
Un desarrollador encuentra un módulo con cobertura de pruebas deficiente. Mientras intenta refactorizar una función compleja para hacerla más legible, descubre que la función produce un resultado incorrecto cuando el subtotal es $0$. En lugar de aislar el problema, decide cambiar la lógica del algoritmo y renombrar tres clases en el mismo paso de trabajo.
- ¿Por qué esta práctica viola el principio de refactorización de Fowler/Beck?
- Si tras su cambio una prueba de integración lejana comienza a fallar de forma intermitente, ¿qué dificultades enfrentará el equipo para determinar la causa raíz?
- ¿Cuál es el protocolo exacto en tres fases que debió haber seguido el desarrollador?
Pregunta 2: Deduplicación accidental vs conascencia semántica#
Un analizador de código estático señala que dos funciones en tu proyecto comparten una estructura de control idéntica de 8 líneas:
- La función
Acalcula el recargo por pago tardío en una factura vencida. - La función
Bcalcula el tiempo de penalización en segundos que un usuario debe esperar tras fallar múltiples intentos de inicio de sesión.
El equipo propone extraer ambas a una función genérica calculate_penalty_curve(...).
- Analiza si esta duplicación es semántica o accidental.
- ¿Qué ocurrirá en seis meses cuando el departamento de seguridad decida cambiar la penalización de login a una curva exponencial mientras que el departamento financiero exige mantener el recargo de facturas lineal?
- ¿Qué costo arquitectónico introdujo la deduplicación prematura?
Pregunta 3: Ceguera booleana (Boolean Blindness) y combinatorias ilegales#
Imagina una función de registro de usuarios: create_user(name, email, is_admin, send_email, is_active).
- Explica qué problemas cognitivos experimenta un ingeniero que lee una llamada como
create_user("Alice", "alice@example.com", false, true, true). - ¿Cuántos estados posibles modelan esos tres booleanos (2³ = 8)?
- Supón que el negocio establece que un usuario que no esté activo (
is_active == false) jamás debe recibir un correo de bienvenida (send_email == true). ¿Cómo permite el sistema de tipos actual representar ese estado ilegal y cómo rediseñarías la firma en Rust para hacer que el estado inválido sea irrepresentable en tiempo de compilación?
Pregunta 4: Diagnóstico diferencial: Divergent Change vs Shotgun Surgery#
Examina estos dos escenarios en un proyecto web:
- Escenario X: Cada vez que el equipo modifica el formato de almacenamiento de las fechas en la base de datos, tiene que abrir y editar 14 controladores distintos, 6 serializadores y 4 vistas web.
- Escenario Y: Cada vez que se cambia la política de autenticación OAuth, el esquema de comisiones de ventas o el motor de plantillas de correo, siempre hay que editar el archivo
UserManager.rs. - Identifica formalmente cuál escenario corresponde a Shotgun Surgery y cuál a Divergent Change.
- Explica qué principio de diseño se viola en cada caso y cuál es la estrategia arquitectónica para resolver cada uno.
Pregunta 5: Optimización prematura, intuición y evidencia de profiling#
Un programador revisa una función que filtra una lista en memoria de 50 elementos. Afirma que "iterar con un bucle for tradicional es inaceptablemente lento" y reescribe la función utilizando punteros crudos con bloques unsafe, eliminando las verificaciones de límites de Rust (bounds checks).
- Explica por qué este cambio no califica como refactorización.
- ¿Qué riesgos de seguridad e invariantes de memoria se destruyen al introducir
unsafede forma injustificada? - Si no existe un informe de benchmarking con herramientas reproducibles que demuestre que esa función de 50 elementos representa un cuello de botella en el tiempo total de la solicitud, ¿por qué debe rechazarse de inmediato esa modificación en una revisión de código?
Code smell#
Síntoma: Argumento bandera (Flag Argument) que genera ceguera booleana#
// Deliberately problematic code:
pub fn print_report(data: &[u8], is_detailed: bool, is_xml: bool) {
// ...
}
// At the call site:
print_report(&payload, true, false);
- Evidencia observable: Parámetros de tipo primitivo
boolen la firma pública de una función. En los sitios de llamada, los argumentos literalestrueyfalseno ofrecen pista alguna sobre su efecto semántico sin navegar al código fuente de la función. - Hipótesis del problema: La función está asumiendo múltiples responsabilidades funcionales; el argumento bandera actúa como un selector de flujo de ejecución interno para decidir qué operación ejecutar.
- Consecuencia concreta: Quien lee el código comete errores de interpretación frecuentes. Es trivial invertir accidentalmente el orden de los booleanos (
print_report(&payload, false, true)compila sin ninguna advertencia, pero invierte silenciosamente la lógica del reporte). - Tratamiento mínimo justificado:
- Si las operaciones son conceptualmente distintas, dividir la función en dos funciones con nombres claros:
print_summary_report(...)yprint_detailed_report(...). - Si las opciones forman una familia de configuraciones, sustituir los booleanos por enumeraciones algebraicas tipadas:
enum ReportDetail { Summary, Detailed }yenum ReportFormat { Json, Xml }.
- Cuándo NO refactorizar: No elimines un booleano si la función es un predicado puro cuya única semántica evidente es responder a una pregunta binaria clara (por ejemplo,
set_visible(true)oset_active(false)en un componente gráfico), o si la función es privada, tiene un único punto de llamada y no existe posibilidad de ambigüedad ni crecimiento futuro.
Criterio de salida#
Puedes considerar superada esta lección cuando:
- Puedes explicar con fluidez la diferencia fundamental entre un defecto (bug), un olor de código (smell) y una optimización de rendimiento.
- Has establecido una línea base de pruebas en verde antes de refactorizar tu código y puedes demostrar que la suite permanece 100% en verde tras la modificación.
- Tu código reemplaza argumentos booleanos ambiguos por enumeraciones tipadas de Rust (
enum), impidiendo estados incoherentes en compilación. - Puedes argumentar con criterios técnicos por qué no toda duplicación sintáctica debe eliminarse y qué riesgos acarrea la abstracción prematura.
- Todos tus programas y pruebas unitarias compilan y pasan exitosamente con
rustc --test.
Lecturas oficiales#
- Rust Book, pruebas automatizadas: Comprende la estructura del arnés de pruebas de Rust, aserciones y aislamiento.
- Rust Clippy: Explora las advertencias automáticas de Clippy como detectores estáticos de olores de código idiomáticos en Rust.
- Martin Fowler, Refactoring: Improving the Design of Existing Code: La referencia canónica sobre el catálogo de olores y transformaciones estructurales.
- Glosario técnico del curso: Consulta definiciones canónicas de términos avanzados y referencias cruzadas.
Siguiente paso#
Continúa con 14. Límites, dependencias y efectos visibles, donde aprenderás a trazar fronteras arquitectónicas claras entre la lógica pura de tu dominio y los efectos secundarios de infraestructura (HTTP, bases de datos y sistemas de archivos). Para revisar la solución completa de la práctica independiente, consulta la guía de respuestas de diseño.