Las cuatro lecciones anteriores de este módulo dieron a BiblioTech un estilo consistente, un
vocabulario de patrones de diseño, dependencias desacopladas mediante interfaces, y una batería de
pruebas unitarias que verifican su comportamiento sin necesitar una base de datos real. Esta
última lección del Módulo 8 cierra el círculo: enseña qué busca una buena revisión de código,
cataloga los code smells (señales de un diseño que puede mejorar) más comunes, y refactoriza un
método de Biblioteca que mezcla varias responsabilidades —apoyándose, precisamente, en las
pruebas unitarias de la lección anterior como red de seguridad. Con esto, BiblioTech llega al
Módulo 9 (Proyecto Final) con un código no solo funcional, sino revisado, probado y depurado según
todo lo visto en este módulo.
Contenido
- Qué busca una buena revisión de código
- Checklist de revisión
- Code smells comunes
- Refactorización: extraer método y extraer clase
- Las pruebas unitarias como red de seguridad antes de refactorizar
- Ejemplo: refactorizando un método largo de
Biblioteca - Cierre del Módulo 8 y enlace con el Proyecto Final
- Qué busca una buena revisión de código
Una revisión de código (code review) es el proceso de que otra persona (o, en un proyecto
individual, uno mismo con perspectiva fresca) lea un cambio antes de incorporarlo definitivamente
al proyecto, buscando problemas que el autor original, inmerso en el detalle, puede no haber
visto. Una buena revisión no busca imponer gustos personales de estilo —eso ya lo automatiza
EditorConfig (Lección 1)—, sino verificar cuatro aspectos concretos:
| Aspecto | Pregunta que se hace el revisor |
|---|---|
| Legibilidad | ¿Se entiende qué hace este código sin tener que ejecutarlo mentalmente paso a paso? |
| Pruebas | ¿Existen pruebas unitarias (Lección 4) que cubran el comportamiento nuevo o modificado? |
| Adherencia a estándares | ¿Sigue las convenciones de nomenclatura, documentación y nulabilidad ya establecidas (Lección 1)? |
| Diseño | ¿Alguna clase o método ha crecido hasta mezclar responsabilidades que deberían separarse (Lección 2, responsabilidad única)? |
Una revisión centrada en estos cuatro aspectos, y no en preferencias personales de estilo, es más rápida, más objetiva, y genera menos fricción entre quien escribe el código y quien lo revisa.
- Checklist de revisión
Una checklist concreta ayuda a que una revisión no dependa solo de la intuición del revisor. Una checklist razonable para un cambio en BiblioTech:
- [ ] ¿El nombre de cada método y variable nuevos sigue las convenciones de la Lección 1
(
PascalCase/camelCase, prefijosI/_, sufijoAsync)? - [ ] ¿Hay algún método que haga más de una cosa claramente separable (validar, persistir, notificar...)?
- [ ] ¿Las dependencias externas (persistencia, servicios) se reciben inyectadas, o se crean
directamente con
newdentro de la clase (Lección 3)? - [ ] ¿El cambio incluye pruebas unitarias nuevas, o modifica pruebas existentes que ya no reflejan el comportamiento nuevo (Lección 4)?
- [ ] ¿La nulabilidad (
?) refleja fielmente qué puede devolvernully qué no? - [ ] ¿Los comentarios explican decisiones no evidentes, o solo repiten lo que el código ya dice (Lección 1)?
Esta lista no es exhaustiva ni universal —cada equipo ajusta la suya con el tiempo—, pero sirve como punto de partida concreto, más útil que una revisión sin ningún criterio explícito.
- Code smells comunes
Un code smell (literalmente, "olor de código") es una señal superficial en el código que sugiere, sin ser en sí misma un error, que el diseño subyacente podría mejorarse. Cuatro code smells especialmente comunes:
| Code smell | Cómo se reconoce | Por qué es un problema |
|---|---|---|
| Método largo | Un método de decenas de líneas, con varios bloques claramente separables | Difícil de leer de una sola vez, difícil de probar de forma aislada (cada bloque necesitaría su propia prueba) |
| Duplicación | El mismo fragmento de lógica, copiado (quizás con pequeñas variaciones) en varios lugares | Un cambio de esa lógica obliga a recordar actualizar cada copia; es fácil olvidar alguna |
| Clase con demasiadas responsabilidades | Una clase que mezcla, por ejemplo, lógica de dominio, persistencia y presentación a la vez | Viola el principio de responsabilidad única (Lección 1); cambiar una responsabilidad arriesga romper las demás sin relación |
| Parámetros excesivos | Un método con seis o más parámetros, varios de ellos relacionados entre sí | Sugiere que esos parámetros deberían agruparse en un objeto propio (por ejemplo, un record, Módulo 3) |
Ninguno de estos smells es, por sí solo, un error que impida compilar o ejecutar el programa —de ahí que se llamen "olores" y no "errores"—; son señales que conviene investigar, no reglas absolutas que obliguen siempre a refactorizar de inmediato.
- Refactorización: extraer método y extraer clase
Refactorizar significa cambiar la estructura interna del código sin cambiar su comportamiento observable: el programa sigue haciendo exactamente lo mismo desde fuera, pero su código interno queda mejor organizado. Dos técnicas de refactorización cubren la mayoría de los smells del apartado anterior:
- Extraer método: tomar un fragmento de un método largo y convertirlo en un método propio, con un nombre que explique qué hace ese fragmento. Resuelve directamente el smell de "método largo".
- Extraer clase: cuando una clase mezcla varias responsabilidades (el smell de "demasiadas
responsabilidades"), mover parte de sus miembros a una clase nueva dedicada a esa
responsabilidad —exactamente lo que ya hizo la Lección 3 al extraer
IRepositorioBibliotecay sus implementaciones fuera deBiblioteca.
// Antes de "extraer metodo": un fragmento de logica de formato mezclado con otra logica
Console.WriteLine($"Prestamo #{prestamo.Libro.Titulo}: prestado el {prestamo.FechaPrestamo:dd/MM/yyyy}" +
(prestamo.FechaDevolucion is not null ? $", devuelto el {prestamo.FechaDevolucion:dd/MM/yyyy}" : ", en curso"));
// Despues de "extraer metodo": el fragmento tiene ahora un nombre propio
string DescribirPrestamo(Prestamo prestamo)
{
string estado = prestamo.FechaDevolucion is not null
? $"devuelto el {prestamo.FechaDevolucion:dd/MM/yyyy}"
: "en curso";
return $"Prestamo #{prestamo.Libro.Titulo}: prestado el {prestamo.FechaPrestamo:dd/MM/yyyy}, {estado}";
}
Console.WriteLine(DescribirPrestamo(prestamo));DescribirPrestamo no cambia el mensaje mostrado por consola —el comportamiento observable es
idéntico—, pero ahora tiene un nombre que explica su propósito, y puede reutilizarse en cualquier
otro punto del programa que necesite el mismo formato, sin duplicar la lógica.
- Las pruebas unitarias como red de seguridad antes de refactorizar
La pregunta que debería surgir siempre antes de refactorizar es: ¿cómo sé que no he roto nada?
Sin pruebas, la única respuesta es "ejecutando la aplicación entera a mano y confiando en no haber
pasado nada por alto" —lento, y poco fiable. Con las pruebas unitarias de la Lección 4 ya
escritas sobre Prestamo.RegistrarDevolucion() y Biblioteca.PrestarLibroAsync, refactorizar deja
de ser un salto de fe:
flowchart LR
A["Pruebas existentes en verde"] --> B["Refactorizar el codigo interno"]
B --> C{"Pruebas siguen en verde?"}
C -->|"Si"| D["El comportamiento no cambio: refactorizacion segura"]
C -->|"No"| E["Algo cambio sin querer: revisar antes de continuar"]
Este es el papel exacto de las pruebas unitarias como red de seguridad: no impiden cometer un error al refactorizar, pero lo detectan de inmediato —en segundos, al volver a ejecutar el mismo conjunto de pruebas— en vez de descubrirlo mucho más tarde, quizás ya en producción. Refactorizar código sin ninguna prueba que lo respalde no es imposible, pero es bastante más arriesgado: cada cambio depende únicamente de la atención del programador en ese momento.
- Ejemplo: refactorizando un método largo de
Biblioteca
BibliotecaImagina que, con las prisas de ir añadiendo funcionalidad módulo a módulo, Biblioteca terminó
con un método que mezcla validación, registro del préstamo y notificación, todo junto:
// Antes: un metodo largo que valida, registra y notifica, todo mezclado
public async Task GestionarPrestamoAsync(Libro libro, Socio socio)
{
// Validacion
if (libro is null)
{
throw new ArgumentNullException(nameof(libro));
}
if (socio is null)
{
throw new ArgumentNullException(nameof(socio));
}
if (!libro.Disponible)
{
throw new InvalidOperationException($"'{libro.Titulo}' no esta disponible para prestamo.");
}
// Espera simulada
await Task.Delay(1000);
// Registro
libro.Prestar();
Prestamo prestamo = new Prestamo(libro, socio);
Prestamos.Add(prestamo);
PrestamoRegistrado?.Invoke(prestamo);
// Persistencia
_repositorio.GuardarCatalogo(Catalogo);
// Notificacion por consola
Console.WriteLine($"'{libro.Titulo}' prestado correctamente a {socio.Nombre}.");
}Este método funciona, y las pruebas de la lección anterior probablemente ya lo cubrirían con algún ajuste menor —pero mezcla, en un único bloque de código, cuatro responsabilidades distintas (validar, esperar, registrar+persistir, mostrar un mensaje), lo que lo hace difícil de leer de un vistazo y difícil de probar de forma aislada. Aplicando "extraer método" sobre cada bloque:
// Despues: cada responsabilidad tiene su propio metodo, con un nombre que la explica
public async Task GestionarPrestamoAsync(Libro libro, Socio socio)
{
ValidarPrestamo(libro, socio);
await Task.Delay(1000); // simulacion de una verificacion lenta, Modulo 4
Prestamo prestamo = RegistrarYPersistirPrestamo(libro, socio);
Console.WriteLine($"'{libro.Titulo}' prestado correctamente a {socio.Nombre}.");
}
private void ValidarPrestamo(Libro libro, Socio socio)
{
ArgumentNullException.ThrowIfNull(libro);
ArgumentNullException.ThrowIfNull(socio);
if (!libro.Disponible)
{
throw new InvalidOperationException($"'{libro.Titulo}' no esta disponible para prestamo.");
}
}
private Prestamo RegistrarYPersistirPrestamo(Libro libro, Socio socio)
{
libro.Prestar();
Prestamo prestamo = new Prestamo(libro, socio);
Prestamos.Add(prestamo);
PrestamoRegistrado?.Invoke(prestamo);
_repositorio.GuardarCatalogo(Catalogo);
return prestamo;
}El comportamiento observable no ha cambiado en absoluto: las mismas excepciones se lanzan en
los mismos casos, el mismo mensaje se muestra por consola, el mismo evento se dispara y el mismo
repositorio se invoca. Lo que ha cambiado es que ahora GestionarPrestamoAsync se lee casi como
una lista de pasos con nombre propio (ValidarPrestamo, RegistrarYPersistirPrestamo), y cada uno
de esos pasos podría probarse por separado si hiciera falta más granularidad en el futuro. Al
ejecutar de nuevo las pruebas de la Lección 4 (PrestarLibroAsync_ConLibroDisponible_GuardaElCatalogo
y PrestarLibroAsync_ConLibroNoDisponible_LanzaExcepcionYNoGuarda, adaptadas al nuevo nombre del
método) sobre esta versión refactorizada, deberían seguir pasando exactamente igual que antes: esa
es la confirmación concreta de que la refactorización fue segura.
- Cierre del Módulo 8 y enlace con el Proyecto Final
Con esta lección se cierra el Módulo 8 (Mejores Prácticas y Patrones de Diseño). A lo largo de sus cinco lecciones, BiblioTech no ha ganado ni una sola funcionalidad de dominio nueva —el objetivo explícito de este módulo era otro: dar un paso atrás y consolidar todo lo construido en los módulos anteriores. El recorrido completo:
| Lección | Qué aportó |
|---|---|
| 1. Estándares de codificación | Nomenclatura consistente, EditorConfig, responsabilidad única, comentarios útiles, documentación XML, nulabilidad consistente |
| 2. Patrones de diseño | Vocabulario común (Singleton, Factory Method, Adapter, Decorator, Strategy, Observer) y reconocimiento de que PrestamoRegistrado ya era un Observer |
| 3. Inyección de dependencias | IRepositorioBiblioteca desacoplando Biblioteca de la persistencia concreta, y el contenedor de DI de ASP.NET Core en profundidad |
| 4. Pruebas unitarias | xUnit, Arrange-Act-Assert, y mocks de Moq sustituyendo IRepositorioBiblioteca en las pruebas |
| 5. Revisión y refactorización | Checklist de revisión, code smells, y refactorización de un método largo apoyada en las pruebas ya existentes |
El Módulo 9 (Proyecto Final) retoma ahora todo lo construido en el curso —el dominio completo desde el Módulo 2, la persistencia del Módulo 5, las cinco interfaces del Módulo 7, y las prácticas de este Módulo 8— para construir la versión final y completa de BiblioTech: se definirán sus requisitos con precisión, se planificará su implementación, se construirá siguiendo los estándares y patrones ya aprendidos, se probará con la misma disciplina de pruebas unitarias vista aquí, y finalmente se desplegará. Nada de lo aprendido en este módulo queda aparte: es, precisamente, el conjunto de prácticas con las que se construirá esa versión final.
Errores Comunes y Consejos
- Refactorizar sin ninguna prueba que respalde el cambio: sin pruebas previas, no hay forma objetiva de confirmar que el comportamiento no cambió; en ese caso, escribir primero al menos las pruebas más importantes sobre el comportamiento actual, y refactorizar después.
- Cambiar comportamiento "de paso" mientras se refactoriza: refactorizar y corregir un error real son dos actividades distintas; mezclarlas en el mismo cambio dificulta saber, si algo falla después, si fue la refactorización o la corrección la causa.
- Revisar código fijándose solo en el estilo: una revisión centrada únicamente en espacios,
nombres o formato (automatizable con
EditorConfig, Lección 1) desaprovecha la oportunidad de detectar problemas de diseño, ausencia de pruebas, o riesgos reales. - Extraer métodos hasta el extremo: dividir un método en fragmentos tan pequeños que hace falta saltar entre diez métodos distintos para entender un flujo simple también dificulta la lectura; el objetivo es claridad, no fragmentación por sí misma.
- Consejo: si dudas si un método necesita refactorizarse, pregúntate si podrías explicarlo en una frase corta; si la respuesta requiere un "y también..." varias veces, probablemente mezcla más de una responsabilidad.
Ejercicios
-
Identifica, en el método
GestionarPrestamoAsync"antes" del apartado 6, qué code smell* del apartado 3 describe mejor su problema principal, y explica en una frase por qué. -
El siguiente método de un
Sociomezcla registrar una sanción con mostrar un mensaje por consola. Refactorízalo con "extraer método", separando ambas responsabilidades:public void AplicarSancion(decimal importe) { SaldoPendiente += importe; Console.WriteLine($"Se ha aplicado una sancion de {importe:C} a {Nombre}. Saldo pendiente: {SaldoPendiente:C}"); }
Soluciones
El code smell principal es método largo (con una responsabilidad mezclada adicional, "demasiadas responsabilidades" a nivel de método): un único método valida, espera, registra, persiste y notifica, todo en el mismo bloque de código, lo que dificulta leerlo y probarlo por partes.
public void AplicarSancion(decimal importe)
{
RegistrarSancion(importe);
NotificarSancion(importe);
}
private void RegistrarSancion(decimal importe)
{
SaldoPendiente += importe;
}
private void NotificarSancion(decimal importe)
{
Console.WriteLine($"Se ha aplicado una sancion de {importe:C} a {Nombre}. Saldo pendiente: {SaldoPendiente:C}");
}
Conclusión
En esta lección has visto qué busca una buena revisión de código (legibilidad, pruebas,
adherencia a estándares, diseño), una checklist concreta para aplicarla, los code smells más
comunes (método largo, duplicación, clases con demasiadas responsabilidades, parámetros
excesivos), dos técnicas de refactorización (extraer método y extraer clase), y cómo las pruebas
unitarias de la lección anterior actúan como red de seguridad al refactorizar un método largo de
Biblioteca sin cambiar su comportamiento observable.
Con esto se cierra el Módulo 8 al completo. BiblioTech llega al Módulo 9 —el Proyecto Final del curso— con un dominio sólido, persistencia desacoplada, una batería de pruebas unitarias, y un código revisado según los estándares y patrones aprendidos en estas cinco lecciones: todo lo necesario para construir, planificar, probar y desplegar la versión completa y definitiva de BiblioTech.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
