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

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

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

Начать курс

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

Работа с устаревшим (legacy) кодом на Java — одна из самых частых и сложных задач, с которой сталкиваются разработчики. Часто такой код не имеет тестов, написан в монолитном стиле и содержит множество антипаттернов. Однако полное переписывание системы — это дорого и рискованно. Гораздо эффективнее применить методичный рефакторинг. В этой статье мы разберем практический подход к рефакторингу legacy Java-кода, который позволит сделать его чище, безопаснее и поддерживаемее.

1. Подготовка: анализ и создание "сети безопасности"

Прежде чем менять код, необходимо понять, что он делает и как не сломать существующую логику. Главный принцип рефакторинга legacy кода — не изменять поведение. Для этого нам нужно создать "сеть безопасности" — набор тестов.

Начните с изучения кода. Найдите входные точки (методы, которые вызываются извне) и попробуйте определить ожидаемые результаты. Даже если код написан плохо, он что-то делает правильно.

После анализа напишите характеризующие тесты (Characterization Tests). Это тесты, которые фиксируют текущее поведение кода. Они не проверяют, правильно ли работает логика, а лишь документируют, что происходит сейчас.

Пример: у нас есть класс-монстр, который обрабатывает заказы.

public class OrderProcessor {
    public void process(Order order) {
        // 200 строк спагетти-кода
        if (order.getTotal() > 100) {
            order.setStatus("VIP");
        } else {
            order.setStatus("STANDARD");
        }
        // ... много другой логики
    }
}

Мы не знаем, что внутри, но можем написать тест:

@Test
public void testProcessOrderWithHighTotal() {
    // Given
    Order order = new Order();
    order.setTotal(150);
    OrderProcessor processor = new OrderProcessor();

    // When
    processor.process(order);

    // Then (фиксируем текущее поведение)
    assertEquals("VIP", order.getStatus());
}

Запустите тесты. Если они проходят — вы зафиксировали текущее поведение. Теперь у вас есть защита от регрессии.

2. Выделение метода (Extract Method)

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

Рассмотрим пример плохого кода:

public double calculateEmployeePay(Employee emp) {
    double basePay = emp.getBasePay();
    double overtimePay = 0;
    if (emp.getHoursWorked() > 40) {
        overtimePay = (emp.getHoursWorked() - 40) * basePay * 1.5;
    }
    double tax = basePay * 0.13;
    double insurance = 5000;
    return basePay + overtimePay - tax - insurance;
}

Здесь смешаны расчет переработок, налогов и страховки. Разделим их:

public double calculateEmployeePay(Employee emp) {
    double basePay = emp.getBasePay();
    double overtimePay = calculateOvertimePay(emp, basePay);
    double tax = calculateTax(basePay);
    double insurance = getInsuranceCost();
    return basePay + overtimePay - tax - insurance;
}

private double calculateOvertimePay(Employee emp, double basePay) {
    if (emp.getHoursWorked() > 40) {
        return (emp.getHoursWorked() - 40) * basePay * 1.5;
    }
    return 0;
}

private double calculateTax(double basePay) {
    return basePay * 0.13;
}

private double getInsuranceCost() {
    return 5000;
}

Теперь каждый метод отвечает за одну задачу. Код стал читаемее, и его легче тестировать по отдельности.

3. Удаление дублирования (DRY)

Legacy код часто страдает от копирования-вставки. Один и тот же кусок логики может встречаться в разных классах. Это опасно: при изменении требований вы можете забыть исправить один из дубликатов.

Предположим, в двух разных сервисах есть код для валидации email:

// В UserService
public boolean isValidEmail(String email) {
    return email != null && email.contains("@") && email.length() > 5;
}

// В NotificationService
public boolean checkEmail(String email) {
    if (email == null) return false;
    return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}

Две разные реализации одной и той же бизнес-логики. Решение — вынести в единый утилитарный класс:

public final class EmailValidator {
    private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@(.+)$";
    private static final Pattern pattern = Pattern.compile(EMAIL_PATTERN);

    private EmailValidator() {}

    public static boolean isValid(String email) {
        if (email == null || email.isBlank()) {
            return false;
        }
        return pattern.matcher(email).matches();
    }
}

После этого замените все разрозненные проверки на вызов EmailValidator.isValid(email). Это централизует логику и упростит её изменение в будущем.

4. Разделение классов (Extract Class)

Часто в legacy коде один класс берет на себя слишком много обязанностей (нарушение Single Responsibility Principle). Например, класс ReportGenerator может генерировать отчет, отправлять его по email и сохранять в базу данных. Это три разные ответственности.

Рассмотрим пример:

public class ReportGenerator {
    public void generateAndSend(String data) {
        // 1. Генерация PDF
        String report = generatePdf(data);
        
        // 2. Отправка email
        EmailSender sender = new EmailSender();
        sender.send("admin@example.com", report);
        
        // 3. Сохранение в БД
        saveToDatabase(report);
    }
    
    private String generatePdf(String data) {
        // логика генерации
        return "PDF_" + data;
    }
    
    private void saveToDatabase(String report) {
        // логика сохранения
    }
}

Выделим каждый аспект в отдельный класс:

// Класс для генерации отчета
public class ReportBuilder {
    public String build(String data) {
        // логика генерации
        return "PDF_" + data;
    }
}

// Класс для отправки
public class ReportSender {
    public void send(String report) {
        EmailSender sender = new EmailSender();
        sender.send("admin@example.com", report);
    }
}

// Класс для хранения
public class ReportRepository {
    public void save(String report) {
        // логика сохранения
    }
}

// Теперь ReportGenerator делегирует задачи
public class ReportGenerator {
    private final ReportBuilder builder;
    private final Report                

Блоги

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