Введение: когда код превращается в 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;
}
}