Рефакторинг legacy кода на Java: продвинутый уровень
Работа с унаследованным (legacy) кодом — неизбежная реальность для любого профессионального Java-разработчика. Часто такой код лишен тестов, содержит запутанные зависимости, монолитные классы и устаревшие паттерны. Простой рефакторинг «на глаз» опасен — он может привести к регрессии и остановке бизнес-процессов. В этой статье мы разберем продвинутые техники рефакторинга legacy Java-кода, которые помогут вам безопасно модернизировать систему, не нарушая её работу.
1. Стратегия «Три шага»: защита перед изменениями
Первый и самый важный принцип работы с legacy кодом — безопасность. Никогда не начинайте рефакторинг без защитной сетки. Используйте стратегию «Три шага»:
- Понять и изолировать — выявить границы изменяемого модуля.
- Покрыть тестами (Characterization Tests) — зафиксировать текущее поведение.
- Рефакторинг — только после этого вносить изменения.
Для написания 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