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

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

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

Начать курс

Рефакторинг legacy кода на Java: продвинутый уровень

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

1. Стратегия «Три шага»: защита перед изменениями

Первый и самый важный принцип работы с legacy кодом — безопасность. Никогда не начинайте рефакторинг без защитной сетки. Используйте стратегию «Три шага»:

  1. Понять и изолировать — выявить границы изменяемого модуля.
  2. Покрыть тестами (Characterization Tests) — зафиксировать текущее поведение.
  3. Рефакторинг — только после этого вносить изменения.

Для написания characterization-тестов используйте подход «золотого мастер-теста» (Golden Master). Запустите код с набором входных данных, сохраните вывод. Затем, после рефакторинга, сравните новый вывод с эталоном.

// Пример: Characterization Test для legacy метода
public class LegacyOrderProcessorTest {

    @Test
    public void testLegacyProcessOrder() {
        // 1. Фиксируем входные данные
        Order order = new Order("LEGACY-001", List.of("item1", "item2"));
        LegacyOrderProcessor processor = new LegacyOrderProcessor();

        // 2. Запускаем legacy код
        String result = processor.processOrder(order);

        // 3. Сравниваем с заранее сохраненным эталоном (Golden Master)
        String expected = "Processed: LEGACY-001 - items: [item1, item2]";
        assertEquals(expected, result);
    }
}

Этот тест не проверяет «правильность» логики, а лишь фиксирует её текущее состояние. После рефакторинга тест должен проходить, гарантируя сохранение поведения.

2. Разрушение монолитных классов: техника «Выделение интерфейса» (Extract Interface)

Крупные классы с десятками методов и множеством зависимостей — типичный признак legacy кода. Вместо того чтобы пытаться переписать всё сразу, используйте Extract Interface.

Создайте интерфейс, описывающий публичный контракт класса. Затем замените прямое использование класса на интерфейс. Это позволит:

  • Изолировать зависимости для тестирования.
  • Постепенно выносить реализацию в новые, более мелкие классы.
  • Внедрять Mock-объекты без модификации клиентского кода.
// Исходный legacy класс
public class HugeDataManager {
    public void saveData(String data) { /* ... сложная логика */ }
    public String loadData(String key) { /* ... */ }
    public void deleteAll() { /* ... опасная операция */ }
    // ... еще 50 методов
}

// Шаг 1: Выделяем интерфейс
public interface DataRepository {
    void saveData(String data);
    String loadData(String key);
}

// Шаг 2: Legacy класс реализует интерфейс
public class HugeDataManager implements DataRepository {
    @Override
    public void saveData(String data) { /* ... */ }
    @Override
    public String loadData(String key) { /* ... */ }
    // deleteAll() остается только в классе, не в интерфейсе
}

// Шаг 3: Клиентский код работает через интерфейс
public class DataService {
    private final DataRepository repository;

    public DataService(DataRepository repository) {
        this.repository = repository;
    }
}

Теперь вы можете написать тест для DataService, используя Mock для DataRepository, не трогая сложный HugeDataManager.

3. Работа с устаревшими коллекциями и циклами

Legacy Java-код (Java 7 и ниже) часто использует устаревшие конструкции: Vector, Hashtable, StringBuffer и ручные циклы for с индексами. Современный рефакторинг предполагает замену их на Stream API и современные коллекции.

Пример: замена ручного цикла на Stream API

// Legacy код
public List<String> filterNames(List<String> names, String prefix) {
    List<String> result = new ArrayList<>();
    for (int i = 0; i < names.size(); i++) {
        String name = names.get(i);
        if (name != null && name.startsWith(prefix)) {
            result.add(name);
        }
    }
    return result;
}

// После рефакторинга
public List<String> filterNames(List<String> names, String prefix) {
    return names.stream()
                .filter(Objects::nonNull)
                .filter(name -> name.startsWith(prefix))
                .collect(Collectors.toList());
}

Важно: Перед заменой убедитесь, что тесты покрывают все граничные случаи (null, пустой список, регистр символов).

4. Избавление от глобального состояния и синглтонов

Legacy код часто злоупотребляет паттерном Singleton и статическими методами для доступа к конфигурации, БД или логгеру. Это делает код практически непроверяемым. Для рефакторинга используйте Dependency Injection (DI) вручную или через фреймворк (Spring, Guice).

Стратегия «Параметризация» (Parameterize Constructor):

// Legacy: синглтон + статический доступ
public class ConfigManager {
    private static ConfigManager instance;
    private Properties props;

    private ConfigManager() {
        props = new Properties();
        loadFromFile("config.properties");
    }

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

    public String getProperty(String key) {
        return props.getProperty(key);
    }
}

// Использование в legacy коде
public class ReportGenerator {
    public void generate() {
        String dbUrl = ConfigManager.getInstance().getProperty("db.url");
        // ...
    }
}

// После рефакторинга: внедряем зависимость
public class ReportGenerator {
    private final ConfigManager configManager;

    // Теперь можно передать Mock в тесте
    public ReportGenerator(ConfigManager configManager) {
        this.configManager = configManager;
    }

    public void generate() {
        String dbUrl = configManager                

Блоги

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