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

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

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

Начать курс

Введение: Почему рефакторинг legacy кода на C# — это необходимость

Каждый разработчик .NET рано или поздно сталкивается с legacy кодом. Это не обязательно 20-летний проект на .NET Framework 1.1 — часто «легаси» становится код, написанный год назад в спешке, без тестов и с нарушением принципов SOLID. Работа с таким кодом превращается в кошмар: любое изменение ломает функциональность в неожиданных местах, а разобраться в логике можно только через дебаг.

Рефакторинг — это не переписывание с нуля. Это улучшение внутренней структуры кода без изменения его внешнего поведения. В этой статье мы разберем практические техники рефакторинга legacy кода на C#, которые можно применять сразу, минимизируя риски.

1. Подготовка: Золотое правило рефакторинга

Прежде чем менять код, убедитесь, что у вас есть «страховочная сетка» — набор автоматических тестов. Без них рефакторинг превращается в гадание.

Создание characterization тестов

Если тестов нет, напишите characterization тесты (также известные как golden master или snapshot тесты). Идея проста: запускаем метод с разными входными данными, записываем результат, а затем проверяем, что после рефакторинга результат не изменился.

// Пример characterization теста для legacy метода
[Test]
public void CalculateTotal_ShouldReturnSameResult_AfterRefactoring()
{
    // Arrange
    var order = new Order { Items = new List<Item> { new Item { Price = 100, Quantity = 2 } } };
    var expected = 200; // Заранее известный результат

    // Act
    var result = LegacyOrderCalculator.CalculateTotal(order);

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

Совет: Используйте библиотеки типа Verify или ApprovalTests для автоматического снэпшот-тестирования.

2. Техника №1: Выделение метода (Extract Method)

Самая простая и эффективная техника. Когда вы видите метод длиной в 200 строк с комментариями вроде «// здесь расчет скидки» — это идеальный кандидат.

Пример: Большой метод

public decimal ProcessOrder(Order order)
{
    decimal total = 0;
    foreach (var item in order.Items)
    {
        total += item.Price * item.Quantity;
    }

    // Расчет скидки
    if (order.Customer.IsPremium)
    {
        total *= 0.9m; // 10% скидка
    }
    else if (order.TotalAmount > 1000)
    {
        total *= 0.95m; // 5% скидка
    }

    // Применение налога
    total *= 1.2m; // 20% налог

    return total;
}

После рефакторинга

public decimal ProcessOrder(Order order)
{
    decimal subtotal = CalculateSubtotal(order.Items);
    decimal afterDiscount = ApplyDiscount(subtotal, order.Customer);
    return ApplyTax(afterDiscount);
}

private decimal CalculateSubtotal(IEnumerable<Item> items)
{
    return items.Sum(item => item.Price * item.Quantity);
}

private decimal ApplyDiscount(decimal total, Customer customer)
{
    if (customer.IsPremium)
        return total * 0.9m;
    if (total > 1000)
        return total * 0.95m;
    return total;
}

private decimal ApplyTax(decimal amount)
{
    return amount * 1.2m;
}

Преимущества:

  • Каждый метод имеет одну ответственность
  • Код становится читаемым и тестируемым
  • Логику скидки можно переиспользовать в других местах

3. Техника №2: Замена условной логики на полиморфизм

Legacy код часто содержит длинные цепочки if-else или switch. Это нарушает принцип открытости/закрытости (OCP).

Пример: Switch по типу документа

public decimal CalculateShippingCost(Order order)
{
    switch (order.ShippingType)
    {
        case "Standard":
            return 5;
        case "Express":
            return 15;
        case "Overnight":
            return 30;
        default:
            throw new ArgumentException("Unknown shipping type");
    }
}

После рефакторинга

// Интерфейс стратегии
public interface IShippingStrategy
{
    decimal CalculateCost(Order order);
}

// Конкретные стратегии
public class StandardShipping : IShippingStrategy
{
    public decimal CalculateCost(Order order) => 5;
}

public class ExpressShipping : IShippingStrategy
{
    public decimal CalculateCost(Order order) => 15;
}

public class OvernightShipping : IShippingStrategy
{
    public decimal CalculateCost(Order order) => 30;
}

// Фабрика стратегий
public class ShippingStrategyFactory
{
    private readonly Dictionary<string, IShippingStrategy> _strategies;

    public ShippingStrategyFactory()
    {
        _strategies = new Dictionary<string, IShippingStrategy>
        {
            { "Standard", new StandardShipping() },
            { "Express", new ExpressShipping() },
            { "Overnight", new OvernightShipping() }
        };
    }

    public IShippingStrategy GetStrategy(string type)
    {
        if (!_strategies.TryGetValue(type, out var strategy))
            throw new ArgumentException($"Unknown shipping type: {type}");
        return strategy;
    }
}

// Использование
public class OrderService
{
    private readonly ShippingStrategyFactory _factory;

    public OrderService(ShippingStrategyFactory factory)
    {
        _factory = factory;
    }

    public decimal CalculateShippingCost(Order order)
    {
        var strategy = _factory.GetStrategy(order.ShippingType);
        return strategy.CalculateCost(order);
    }
}

Плюсы:

  • Легко добавлять новые типы доставки без изменения существующего кода
  • Каждая стратегия тестируется отдельно
  • Устраняется «запах кода» — длинный switch

4. Техника №3: Внедрение зависимостей (Dependency Injection)

Классическая проблема legacy кода — жесткие зависимости. Например, прямое обращение к базе данных или файловой системе внутри бизнес-логики.

Пример: Жесткая зависимость от БД

public class UserService
{
    public bool IsUserActive(int userId)
    {
        // Прямое создание соединения с БД
        using (var connection = new SqlConnection("connection_string"))
        {
            connection.Open();
            var command = new SqlCommand("SELECT IsActive FROM Users WHERE Id = @id", connection);
            command.Parameters.AddWithValue("@id", userId);
            return (bool)command.ExecuteScalar();
        }
    }                

Блоги

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