Рефакторинг legacy кода на C#: Как превратить устаревший проект в современный .NET-продукт
Каждый разработчик .NET рано или поздно сталкивается с legacy-кодом — монолитными классами на 3000 строк, спагетти-логикой в файлах .aspx.cs и магическими строками, разбросанными по всему проекту. Рефакторинг такого кода — это не просто "причесывание" синтаксиса, а стратегическая задача: улучшить архитектуру, не сломав бизнес-логику. В этой статье мы разберем практические подходы к рефакторингу C# кода, которые помогут вам безопасно и эффективно модернизировать унаследованные проекты.
1. Подготовка: Почему рефакторинг legacy кода требует особого подхода
Главная ошибка новичков — сразу бросаться переписывать код. В C# .NET legacy-проекты часто содержат скрытые зависимости: статические классы, глобальные состояния, жесткие связи с базой данных через ADO.NET. Любое изменение может вызвать каскад ошибок.
Основные признаки legacy кода в C#:
- Отсутствие модульных тестов — код не покрыт юнит-тестами, что делает рефакторинг рискованным.
- Божественные объекты (God Objects) — классы, которые делают всё: от работы с БД до рендеринга UI.
- Жесткие зависимости — прямое создание экземпляров через
new SqlConnection()внутри методов. - Магические строки и числа — конфигурации, вшитые в код, а не в
app.configилиappsettings.json. - Использование устаревших API — например,
WebClientвместоHttpClientилиArrayListвместоList<T>.
Перед началом рефакторинга обязательно: заморозьте требования у заказчика, настройте систему контроля версий (Git) и создайте "страховочную сетку" в виде интеграционных тестов.
2. Безопасный рефакторинг: Стратегия "Передвижение стены"
Классическая техника для C# — Strangler Fig Pattern (Паттерн "Удушающий плющ"). Вместо того чтобы переписывать всё сразу, вы постепенно заменяете старые компоненты новыми, пока старый код не перестанет использоваться.
2.1. Выделение интерфейсов и внедрение зависимостей
Самый частый антипаттерн в legacy C# — статические методы-помощники. Рассмотрим пример:
// Устаревший код
public class OrderService
{
public void ProcessOrder(int orderId)
{
var conn = new SqlConnection("Server=myServer;Database=myDB;...");
var cmd = new SqlCommand("SELECT * FROM Orders WHERE Id = " + orderId, conn);
// ... тонны логики
}
}
Шаг 1: Выделяем интерфейс для доступа к данным.
public interface IOrderRepository
{
Order GetById(int orderId);
}
public class OrderRepository : IOrderRepository
{
private readonly string _connectionString;
public OrderRepository(string connectionString)
{
_connectionString = connectionString;
}
public Order GetById(int orderId)
{
using var conn = new SqlConnection(_connectionString);
// ... безопасный запрос
}
}
Шаг 2: Внедряем зависимость через конструктор. Теперь вы можете подменить репозиторий в тестах, а сам OrderService перестал зависеть от конкретной реализации БД.
public class OrderService
{
private readonly IOrderRepository _orderRepository;
public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void ProcessOrder(int orderId)
{
var order = _orderRepository.GetById(orderId);
// ... бизнес-логика
}
}
2.2. Замена устаревших типов на современные
В .NET Framework часто использовались ArrayList, Hashtable и StringBuilder (хотя последний всё ещё актуален). При рефакторинге на .NET 6+ обязательно замените их на типизированные коллекции:
// Было (устаревшее)
ArrayList items = new ArrayList();
items.Add("text");
string first = (string)items[0]; // Грязный кастинг
// Стало (современное)
List<string> items = new List<string>();
items.Add("text");
string first = items[0]; // Безопасно и читаемо
3. Рефакторинг бизнес-логики: Избавляемся от "спагетти"
Часто legacy код — это один гигантский метод, который делает всё. Наша задача — разбить его на маленькие, тестируемые куски.
3.1. Техника "Извлечение метода" (Extract Method)
Представьте метод, который одновременно проверяет права пользователя, вычисляет скидку и отправляет email:
public void Checkout(int userId, int productId)
{
var user = GetUser(userId);
if (user.IsBlocked) throw new Exception("User blocked");
var product = GetProduct(productId);
decimal discount = 0;
if (user.IsPremium) discount = 0.1m;
var total = product.Price * (1 - discount);
// ... ещё 50 строк кода
}
После рефакторинга:
public void Checkout(int userId, int productId)
{
var user = GetUser(userId);
ValidateUser(user);
var product = GetProduct(productId);
var discount = CalculateDiscount(user);
var total = CalculateTotal(product, discount);
SendConfirmationEmail(user.Email, total);
}
private void ValidateUser(User user)
{
if (user.IsBlocked) throw new UserBlockedException(user.Id);
}
private decimal CalculateDiscount(User user)
{
return user.IsPremium ? 0.1m : 0m;
}
private decimal CalculateTotal(Product product, decimal discount)
{
return product.Price * (1 - discount);
}
Теперь каждый метод можно протестировать изолированно, а сам код стал самодокументируемым.
3.2. Убираем магические строки и числа
Строки вроде "SELECT * FROM Orders WHERE Status = 'Active'" или числа 0.1m должны быть кон