Введение: что такое legacy код и почему его нужно рефакторить
Каждый Java-разработчик рано или поздно сталкивается с legacy кодом — устаревшим, плохо структурированным и слабо документированным программным обеспечением. Такой код часто написан без unit-тестов, с нарушением принципов SOLID, и его поддержка превращается в кошмар. Рефакторинг legacy кода на Java — это не просто прихоть, а необходимость для снижения технического долга и ускорения разработки новых фич.
В этой статье мы разберем рефакторинг legacy кода на Java от А до Я: от выявления проблемных участков до безопасного внедрения изменений с помощью современных инструментов.
Этап 1: Анализ и оценка legacy кода
Прежде чем менять код, его нужно понять. Вот ключевые шаги анализа:
1.1. Сбор метрик и поиск "запахов" кода
Используйте статические анализаторы:
- SonarQube — выявляет дублирование, сложные методы, нарушения code style
- PMD — находит потенциальные баги и dead code
- Checkstyle — проверяет соответствие стандартам оформления
Пример типичного "запаха" — гигантский метод:
public class OrderProcessor {
public void process(Order order) {
// 200 строк кода: валидация, расчет скидок, отправка email, логирование...
// Разделите это на маленькие методы!
}
}
1.2. Построение карты зависимостей
Legacy код часто содержит циклические зависимости и God-классы. Используйте инструменты визуализации:
- IntelliJ IDEA Dependency Viewer — встроенный инструмент
- Structure101 — профессиональное решение для анализа архитектуры
Этап 2: Создание защитной сети из тестов
Без тестов рефакторинг — это игра в русскую рулетку. Ваша цель — покрыть ключевые сценарии Characterization Tests (тесты, которые фиксируют текущее поведение).
2.1. Техника "Золотая запись" (Golden Master)
Для больших блоков кода без тестов:
- Запустите систему с реальными данными
- Сохраните вывод (логи, ответы API) как эталон
- После рефакторинга сравните результат с эталоном
2.2. Пример теста для legacy класса
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class LegacyCalculatorTest {
@Test
public void testLegacyCalculation() {
LegacyCalculator calc = new LegacyCalculator();
// Фиксируем текущее поведение
int result = calc.calculate(10, 5);
assertEquals(15, result); // Если баг — тест сохраняет баг!
}
}
Важно: сначала закоммитьте тесты, затем рефакторите. Зеленый тест — ваша страховка.
Этап 3: Пошаговый рефакторинг с паттернами
Рефакторинг legacy кода на Java — это не "все переписать", а серия маленьких безопасных трансформаций.
3.1. Извлечение метода (Extract Method)
Самый безопасный рефакторинг. IDE делает это автоматически.
До:
public void printInvoice(Invoice invoice) {
double total = 0;
for (Item item : invoice.getItems()) {
total += item.getPrice() * item.getQuantity();
}
double discount = total > 100 ? total * 0.1 : 0;
double finalTotal = total - discount;
System.out.println("Total: " + finalTotal);
}
После (с извлечением методов):
public void printInvoice(Invoice invoice) {
double total = calculateTotal(invoice);
double discount = calculateDiscount(total);
double finalTotal = total - discount;
printTotal(finalTotal);
}
private double calculateTotal(Invoice invoice) {
return invoice.getItems().stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
}
private double calculateDiscount(double total) {
return total > 100 ? total * 0.1 : 0;
}
private void printTotal(double total) {
System.out.println("Total: " + total);
}
3.2. Замена условных операторов полиморфизмом
Типичный legacy-антипаттерн — цепочка if-else или switch:
public double calculateShipping(Order order) {
switch (order.getType()) {
case "STANDARD": return 5.0;
case "EXPRESS": return 15.0;
case "INTERNATIONAL": return 25.0;
default: throw new IllegalArgumentException();
}
}
Рефакторинг с паттерном Strategy:
// Интерфейс стратегии
public interface ShippingStrategy {
double calculate(Order order);
}
// Конкретные стратегии
public class StandardShipping implements ShippingStrategy {
@Override
public double calculate(Order order) { return 5.0; }
}
public class ExpressShipping implements ShippingStrategy {
@Override
public double calculate(Order order) { return 15.0; }
}
// Использование
public class ShippingCalculator {
private final Map<String, ShippingStrategy> strategies;
public ShippingCalculator() {
strategies = new HashMap<>();
strategies.put("STANDARD", new StandardShipping());
strategies.put("EXPRESS", new ExpressShipping());
strategies.put("INTERNATIONAL", new InternationalShipping());
}
public double calculate(Order order) {
ShippingStrategy strategy = strategies.get(order.getType());
if (strategy == null) throw new IllegalArgumentException();
return strategy.calculate(order);
}
}
3.3. Разделение God-класса (God Class)
Классы с 3000+ строк — типичный признак legacy. Используйте Extract Class и Facade:
- Создайте новый класс для логики (например,
EmailServiceизOrderProcessor) - Перенесите методы по одному, перекомпилируя после каждого
- В исходном классе оставьте делегирование (Facade)
// После рефакторинга
public class OrderProcessor {
private final EmailService emailService;
private final PaymentService paymentService;
public void process(Order