Рефакторинг legacy кода на C#: продвинутые техники и практики

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: когда код превращается в legacy

Каждый разработчик C#, работающий в крупной компании, рано или поздно сталкивается с проектами, которым 5, 10, а то и 15 лет. Такой код называют legacy. Он работает, приносит деньги, но каждое изменение в нём — риск. Отсутствие тестов, тесная связность (tight coupling), гигантские классы на тысячи строк, магические строки и копипаста — вот реальность legacy.

Простой рефакторинг «на глаз» здесь не пройдёт. Нужны продвинутые стратегии, которые позволят постепенно улучшать код, не ломая бизнес-логику. В этой статье мы разберём 4 ключевых подхода для рефакторинга legacy-систем на C# (.NET) на продвинутом уровне.

1. Стратегия «Strangler Fig»: замена модулей без остановки системы

Паттерн Strangler Fig (удушающий фикус) — это классика для постепенной замены legacy-компонентов. Суть: вы не переписываете всю систему сразу, а постепенно перехватываете вызовы к старым модулям и перенаправляете их на новые.

Как реализовать на C#

Самый простой способ — использовать интерфейсы и фабрики. Допустим, у вас есть старый класс OldOrderProcessor, который используется по всему проекту.

// Старый интерфейс (уже существует)
public interface IOrderProcessor
{
    void Process(Order order);
}

// Старая реализация (legacy)
public class OldOrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        // Много лет работающий, но грязный код
    }
}

Создаём новую реализацию и фабрику с feature toggle:

public class NewOrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        // Новый, чистый код
    }
}

public class OrderProcessorFactory
{
    public IOrderProcessor Create()
    {
        // Пример: переключаем по конфигу
        if (ConfigurationManager.AppSettings["UseNewProcessor"] == "true")
            return new NewOrderProcessor();
        return new OldOrderProcessor();
    }
}

Теперь вы можете включать новый процессор для 5% пользователей, мониторить ошибки и постепенно увеличивать процент. Когда старый код станет не нужен — удалите его.

2. Выделение ядра: Separation of Concerns через Dependency Injection

Legacy-код часто страдает от God-классов (классов-богов) и прямой зависимости от базы данных, файловой системы, внешних API. Внедрение Dependency Injection (DI) — первый шаг к модульности.

Практический пример

Представьте, что у вас есть класс ReportGenerator, который внутри создаёт SqlConnection:

public class ReportGenerator
{
    public void Generate()
    {
        using (var conn = new SqlConnection("..."))
        {
            // запросы
        }
    }
}

Это невозможно тестировать без реальной БД. Рефакторинг:

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();
        // формирование отчета
    }
}

Теперь вы можете подменить репозиторий на mock в юнит-тестах. Используйте стандартный DI-контейнер .NET Core (Microsoft.Extensions.DependencyInjection) или Autofac для внедрения зависимостей.

3. Тестирование legacy-кода: Golden Master и Characterization Tests

Без тестов рефакторинг — это прыжок с парашютом без запасного. Для legacy-кода идеально подходит подход Characterization Tests (характеризационные тесты) или Golden Master Testing.

Как это работает

Вы запускаете старый код с набором входных данных, записываете выходные данные (файлы, строки, БД) и сохраняете их как «эталон». Затем после рефакторинга сравниваете результат с эталоном.

Пример для консольного приложения:

[Test]
public void GoldenMaster_ProcessOrder_ShouldMatchLegacyOutput()
{
    // Arrange
    var input = File.ReadAllText("test_data/order_input.json");
    var expectedOutput = File.ReadAllText("test_data/expected_output.txt");

    // Act
    var processor = new RefactoredOrderProcessor();
    var result = processor.Process(input);

    // Assert
    Assert.AreEqual(expectedOutput, result);
}

Совет: используйте Pex (Parameterized Unit Tests) или FsCheck для генерации случайных входных данных и автоматического поиска расхождений.

4. Разбиение монолита: Event Sourcing и CQRS как способ выжить

Когда legacy-система представляет собой один большой монолит, разбить его на микросервисы напрямую — задача на годы. Вместо этого используйте Event Sourcing и CQRS (Command Query Responsibility Segregation) для выделения отдельных контекстов.

Пошаговая стратегия

  • Шаг 1. Определите Bounded Context (ограниченный контекст) — например, «Управление заказами».
  • Шаг 2. Начните записывать все изменения состояния в Event Store (например, EventStoreDB).
  • Шаг 3. Постепенно переводите чтение данных на отдельную read-модель.
  • Шаг 4. Когда read-модель станет стабильной, вы можете вынести её в отдельный сервис.

Пример команды на C# с использованием библиотеки MediatR:

public class CreateOrderCommand : IRequest<Guid>
{
    public string CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IEventStore _eventStore;

    public CreateOrderHandler(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var orderId = Guid.NewGuid();
        var orderCreated = new OrderCreatedEvent(orderId, request.CustomerId, request.Items);
        await _eventStore.AppendAsync(orderCreated);
        return orderId;
    }
}

                
                

Блоги

Рекомендаций по книгам