Рефакторинг legacy кода на C#: от А до Я
Работа с унаследованным (legacy) кодом — неизбежная реальность для любого .NET разработчика. Старые проекты на C# часто содержат "спагетти-код", магические числа, огромные классы и отсутствие тестов. Вместо того чтобы переписывать всё с нуля (что дорого и рискованно), грамотный рефакторинг позволяет постепенно улучшать код, снижая технический долг. В этой статье мы разберём пошаговую стратегию рефакторинга legacy кода на C# с конкретными примерами.
1. Подготовка к рефакторингу: безопасность превыше всего
Главное правило рефакторинга legacy кода — не сломать то, что работает. Без тестов любое изменение может привести к багам в продакшене. Поэтому первый этап — создание "сети безопасности".
1.1. Добавление тестов (Characterization Tests)
Если тестов нет, их нужно написать. Используйте подход Characterization Tests (тесты характеризации). Суть: вы запускаете существующий код с разными входными данными и фиксируете текущее поведение как эталон.
// Пример для legacy метода
public class LegacyCalculator
{
public int CalculateTotal(int a, int b)
{
// Legacy код, который мы не понимаем до конца
return a + b * 2; // Подозрительная логика
}
}
// Тест характеризации
[Test]
public void CalculateTotal_WhenCalled_ReturnsCurrentBehavior()
{
var calc = new LegacyCalculator();
int result = calc.CalculateTotal(3, 4);
// Фиксируем текущий результат как ожидаемый
Assert.AreEqual(11, result); // 3 + 4*2 = 11
}
После написания таких тестов вы можете смело рефакторить, зная, что тесты поймают регрессию.
1.2. Изоляция зависимостей
Legacy код часто жёстко завязан на базу данных, файловую систему или внешние сервисы. Используйте Extract Interface и внедрение зависимостей (DI) для изоляции.
// Было: жёсткая зависимость
public class OrderProcessor
{
private readonly SqlDatabase _db = new SqlDatabase();
public void Process(Order order)
{
_db.Save(order);
}
}
// Стало: интерфейс и DI
public interface IOrderRepository
{
void Save(Order order);
}
public class OrderProcessor
{
private readonly IOrderRepository _repository;
public OrderProcessor(IOrderRepository repository)
{
_repository = repository;
}
public void Process(Order order)
{
_repository.Save(order);
}
}
2. Основные техники рефакторинга legacy C# кода
После подготовки можно приступать к улучшению кода. Вот ключевые техники, которые работают с .NET проектами.
2.1. Extract Method (Извлечение метода)
Огромные методы с 200+ строк — классика legacy. Разбивайте их на мелкие, читаемые методы.
// До рефакторинга
public void ProcessInvoice(Invoice invoice)
{
// 50 строк расчёта скидки
decimal discount = 0;
if (invoice.Total > 1000) discount = 0.1m;
else if (invoice.Total > 500) discount = 0.05m;
// ... ещё много кода
// 30 строк отправки email
// ...
}
// После рефакторинга
public void ProcessInvoice(Invoice invoice)
{
decimal discount = CalculateDiscount(invoice.Total);
SendInvoiceEmail(invoice);
}
private decimal CalculateDiscount(decimal total)
{
if (total > 1000) return 0.1m;
if (total > 500) return 0.05m;
return 0;
}
private void SendInvoiceEmail(Invoice invoice)
{
// Логика отправки
}
2.2. Replace Magic Numbers with Constants
Магические числа делают код непонятным. Замените их константами.
// Плохо
if (user.Role == 3) { /* admin */ }
// Хорошо
public enum UserRole
{
Guest = 1,
User = 2,
Admin = 3
}
if (user.Role == (int)UserRole.Admin) { /* admin */ }
2.3. Удаление мёртвого кода
Legacy проекты полны закомментированных блоков и неиспользуемых методов. Удаляйте их без сожаления. Используйте анализаторы кода (Roslyn) для поиска dead code.
3. Работа с большими классами и God-объектами
Классы на 1000+ строк, которые делают всё — бич legacy. Стратегия: Extract Class и Single Responsibility Principle (SRP).
3.1. Выделение классов по ответственности
// God-класс до рефакторинга
public class EmployeeService
{
public void SaveEmployee(Employee emp) { /* SQL */ }
public void SendEmail(Employee emp) { /* SMTP */ }
public void GenerateReport(Employee emp) { /* PDF */ }
public decimal CalculateTax(Employee emp) { /* Финансы */ }
}
// После рефакторинга
public class EmployeeRepository
{
public void Save(Employee emp) { /* SQL */ }
}
public class EmailService
{
public void Send(Employee emp) { /* SMTP */ }
}
public class TaxCalculator
{
public decimal Calculate(Employee emp) { /* Финансы */ }
}
3.2. Использование Partial Classes (временное решение)
Если класс слишком велик и вы не можете сразу его разбить, используйте partial class для разделения на файлы по функциональности. Это временная мера.
// EmployeeService.DataAccess.cs
public partial class EmployeeService
{
public void SaveEmployee(Employee emp) { }
}
// EmployeeService.Notifications.cs
public partial class EmployeeService
{
public void SendEmail(Employee emp) { }
}
4. Рефакторинг условной логики и Switch-конструкций
Длинные if-else и switch — источник багов. Заменяйте их на полиморфизм или паттерн Strategy.
4.1. Замена Switch на Strategy Pattern
// Legacy код
public decimal CalculateShipping(Order order)
{
switch (order.ShippingType)
{
case "Standard": return 5.99m;
case "Express": return 15.99m;
case "Overnight": return 29.99m;
default: throw new ArgumentException();
}
}
// После рефакторинга
public interface I