Introducción: cuando el código se convierte en legacy
Cada desarrollador de C# que trabaja en una gran empresa, tarde o temprano se encuentra con proyectos que tienen 5, 10 o incluso 15 años. A este código se le llama legacy. Funciona, genera dinero, pero cada cambio en él es un riesgo. La falta de pruebas, el acoplamiento fuerte (tight coupling), clases gigantescas de miles de líneas, cadenas mágicas y código copiado y pegado: esa es la realidad del legacy.
Una simple refactorización "a ojo" no funcionará aquí. Se necesitan estrategias avanzadas que permitan mejorar el código gradualmente sin romper la lógica de negocio. En este artículo, analizaremos 4 enfoques clave para la refactorización de sistemas legacy en C# (.NET) a nivel avanzado.
1. Estrategia "Strangler Fig": reemplazo de módulos sin detener el sistema
El patrón Strangler Fig (higuera estranguladora) es un clásico para el reemplazo gradual de componentes legacy. La idea: no reescribes todo el sistema de una vez, sino que interceptas gradualmente las llamadas a los módulos antiguos y las rediriges a los nuevos.
Cómo implementarlo en C#
La forma más sencilla es usar interfaces y fábricas. Supongamos que tienes una clase antigua OldOrderProcessor que se usa en todo el proyecto.
// Interfaz antigua (ya existe)public interface IOrderProcessor{ void Process(Order order);}
// Implementación antigua (legacy)public class OldOrderProcessor : IOrderProcessor{ public void Process(Order order) { // Código que funciona desde hace años, pero es sucio }}Creamos una nueva implementación y una fábrica con un feature toggle:
public class NewOrderProcessor : IOrderProcessor{ public void Process(Order order) { // Código nuevo y limpio }}
public class OrderProcessorFactory{ public IOrderProcessor Create() { // Ejemplo: cambiar según configuración if (ConfigurationManager.AppSettings["UseNewProcessor"] == "true") return new NewOrderProcessor(); return new OldOrderProcessor(); }}Ahora puedes activar el nuevo procesador para el 5% de los usuarios, monitorear errores y aumentar gradualmente el porcentaje. Cuando el código antiguo ya no sea necesario, elimínalo.
2. Extracción del núcleo: Separation of Concerns mediante Dependency Injection
El código legacy a menudo sufre de God-Classes (clases dios) y dependencias directas de la base de datos, el sistema de archivos o APIs externas. La implementación de Dependency Injection (DI) es el primer paso hacia la modularidad.
Ejemplo práctico
Imagina que tienes una clase ReportGenerator que internamente crea un SqlConnection:
public class ReportGenerator{ public void Generate() { using (var conn = new SqlConnection("...")) { // consultas } }}Esto no se puede probar sin una base de datos real. Refactorización:
public interface IDataRepository{ IEnumerable<DataRow> GetReportData();}
public class ReportGenerator{ private readonly IDataRepository _repository;
public ReportGenerator(IDataRepository repository) { _repository = repository; }
public void Generate() { var data = _repository.GetReportData(); // generación del informe }}Ahora puedes reemplazar el repositorio por un mock en las pruebas unitarias. Usa el contenedor DI estándar de .NET Core (Microsoft.Extensions.DependencyInjection) o Autofac para la inyección de dependencias.
3. Pruebas de código legacy: Golden Master y Characterization Tests
Sin pruebas, la refactorización es como saltar en paracaídas sin uno de repuesto. Para el código legacy, el enfoque ideal son las Characterization Tests (pruebas de caracterización) o Golden Master Testing.
Cómo funciona
Ejecutas