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

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

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

Начать курс

Рефакторинг legacy кода на Java: от А до Я

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

1. Диагностика и подготовка: прежде чем трогать код

Первый и самый важный шаг — понять, с чем вы имеете дело. Не начинайте рефакторинг с хаотичной правки строк. Вместо этого:

  • Карта зависимостей: визуализируйте, какие классы от кого зависят. Используйте инструменты вроде IntelliJ IDEA Dependency Viewer или JDepend.
  • Тестовая обертка (Characterization Tests): напишите тесты, которые фиксируют текущее поведение кода. Это ваша страховочная сетка.
  • Выделите точки входа: определите, какие методы вызываются чаще всего и где происходят основные изменения.

Пример простого characterization теста для метода, который нужно отрефакторить:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class LegacyCalculatorTest {

    @Test
    public void testLegacyCalculate() {
        LegacyCalculator calc = new LegacyCalculator();
        // Фиксируем текущее поведение
        int result = calc.calculate(10, 5, "add");
        assertEquals(15, result, "Текущее поведение: сложение");
        
        result = calc.calculate(10, 5, "sub");
        assertEquals(5, result, "Текущее поведение: вычитание");
    }
}

Такой тест не проверяет правильность логики в абсолютном смысле — он просто фиксирует, что код делает сейчас. После рефакторинга тест должен пройти, иначе вы изменили поведение.

2. Стратегия "Хирургического" рефакторинга: метод за методом

Попытка отрефакторить весь класс сразу — верный способ получить баги. Используйте подход "боевой раскопки" (Combat Archaeology):

  • Выберите один метод или небольшую группу связанных методов.
  • Напишите для них characterization тесты (если их нет).
  • Примените одну технику рефакторинга (например, Extract Method или Rename Variable).
  • Запустите тесты. Они должны пройти.
  • Закоммитьте изменения с понятным описанием.

Пример: рефакторинг метода с "магическими числами" и длинным списком параметров.

Исходный код (плохой):

public class OrderProcessor {
    public double process(double amount, int type, double discount) {
        double result;
        if (type == 1) {
            result = amount * 1.2; // 20% наценка
            if (discount > 0.1) {
                result = result - result * 0.05; // 5% скидка
            }
        } else if (type == 2) {
            result = amount * 1.5; // 50% наценка
        } else {
            result = amount;
        }
        return result;
    }
}

После рефакторинга (улучшенный):

public class OrderProcessor {
    private static final double PREMIUM_MARKUP = 1.2;
    private static final double STANDARD_MARKUP = 1.5;
    private static final double LOYALTY_DISCOUNT = 0.05;
    private static final double DISCOUNT_THRESHOLD = 0.1;

    public double process(OrderType type, double amount, double discount) {
        double basePrice = applyMarkup(amount, type);
        return applyLoyaltyDiscount(basePrice, discount);
    }

    private double applyMarkup(double amount, OrderType type) {
        return switch (type) {
            case PREMIUM -> amount * PREMIUM_MARKUP;
            case STANDARD -> amount * STANDARD_MARKUP;
            case BASIC -> amount;
        };
    }

    private double applyLoyaltyDiscount(double price, double discount) {
        if (discount > DISCOUNT_THRESHOLD) {
            return price - price * LOYALTY_DISCOUNT;
        }
        return price;
    }
}

enum OrderType {
    BASIC, PREMIUM, STANDARD
}

Мы заменили магические числа на константы, выделили отдельные методы и ввели enum вместо числового типа. Тесты, фиксирующие старое поведение, должны пройти без изменений.

3. Работа с устаревшими паттернами: замена глобального состояния

Одна из самых частых проблем legacy Java-кода — синглтоны и статические методы. Они создают скрытые зависимости и делают код нетestируемым.

Рассмотрим пример с "классическим" синглтоном:

public class DatabaseConnection {
    private static DatabaseConnection instance;
    private Connection connection;

    private DatabaseConnection() {
        // Сложная инициализация
    }

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }

    public Connection getConnection() {
        return connection;
    }
}

// Использование в коде
public class UserRepository {
    public User findUser(int id) {
        Connection conn = DatabaseConnection.getInstance().getConnection();
        // ... запрос к БД
    }
}

Проблема: невозможно подменить соединение в тестах. Решение — внедрение зависимостей (Dependency Injection).

Рефакторинг:

// Создаем интерфейс
public interface DatabaseProvider {
    Connection getConnection();
}

// Реальная реализация
public class RealDatabaseProvider implements DatabaseProvider {
    @Override
    public Connection getConnection() {
        // Реальная инициализация
        return new Connection();
    }
}

// Класс теперь принимает зависимость
public class UserRepository {
    private final DatabaseProvider dbProvider;

    public UserRepository(DatabaseProvider dbProvider) {
        this.dbProvider = dbProvider;
    }

    public User findUser(int id) {
        Connection conn = dbProvider.getConnection();
        // ... запрос к БД
    }
}

// Теперь легко тестировать
public class UserRepositoryTest {
    @Test
    public void testFindUser() {
        DatabaseProvider mockProvider = mock(DatabaseProvider.class);
        when(mockProvider.getConnection()).thenReturn(new MockConnection());
        
        UserRepository repo = new UserRepository(mockProvider);
        // ... тест
    }
}

Важно: не пытайтесь внедрить DI-контейнер (Spring/Guice) сразу. Сначала сделайте код тестируемым, затем подумайте об инверсии

Блоги

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