Введение: Почему рефакторинг 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();
}
}