Рефакторинг legacy кода на Java: с чего начать и как не сломать проект
Каждый Java-разработчик рано или поздно сталкивается с legacy кодом. Это может быть монолитное приложение, написанное десять лет назад, или проект с устаревшими паттернами, отсутствием тестов и «магическими числами». Рефакторинг такого кода — задача не из легких, но абсолютно необходимая для поддержания жизнеспособности системы.
В этой статье мы разберем основные стратегии рефакторинга legacy кода на Java, покажем конкретные примеры и дадим практические советы, которые помогут вам улучшить код без риска сломать продакшн.
Почему legacy код требует рефакторинга?
Legacy код — это не просто «старый код». Это код, который:
- Сложно поддерживать и расширять
- Не имеет достаточного покрытия тестами
- Содержит дублирование и «запахи» (code smells)
- Использует устаревшие библиотеки и подходы
Рефакторинг позволяет улучшить читаемость, производительность и тестируемость кода. Однако главное правило — не менять внешнее поведение системы. Каждое изменение должно быть безопасным и контролируемым.
Стратегии рефакторинга legacy кода на Java
1. Создание защитной сети из тестов
Прежде чем трогать код, напишите тесты. Это может быть сложно, если код плохо тестируется. Используйте подход Characterization Tests (тесты на основе текущего поведения).
Пример: у вас есть метод, который обрабатывает заказы:
public class OrderProcessor {
public double calculateTotal(Order order) {
double total = 0;
for (Item item : order.getItems()) {
if (item.getPrice() > 100) {
total += item.getPrice() * 0.9; // скидка 10%
} else {
total += item.getPrice();
}
}
if (order.isPremium()) {
total *= 0.95; // дополнительная скидка 5%
}
return total;
}
}
Пишем тест, который фиксирует текущее поведение:
@Test
public void testCalculateTotal_WithPremiumAndExpensiveItems() {
Order order = new Order();
order.addItem(new Item("Laptop", 1000));
order.addItem(new Item("Mouse", 50));
order.setPremium(true);
double expected = (1000 * 0.9 + 50) * 0.95; // 902.5
assertEquals(expected, new OrderProcessor().calculateTotal(order), 0.001);
}
Теперь у вас есть «защитная сетка». Можно рефакторить, не боясь изменить логику.
2. Разделение больших методов (Extract Method)
Одна из самых частых проблем legacy кода — методы-«монстры» длиной в сотни строк. Разбивайте их на небольшие, хорошо именованные методы.
До рефакторинга:
public void processInvoice(Invoice invoice) {
// 50 строк проверки
if (invoice.getCustomer() == null) throw new IllegalArgumentException();
if (invoice.getItems().isEmpty()) throw new IllegalStateException();
// ... еще 20 строк проверок
// 100 строк расчета скидок и налогов
// 80 строк отправки email и сохранения в БД
}
После рефакторинга:
public void processInvoice(Invoice invoice) {
validateInvoice(invoice);
double total = calculateTotal(invoice);
saveInvoice(invoice, total);
sendConfirmationEmail(invoice);
}
private void validateInvoice(Invoice invoice) {
if (invoice.getCustomer() == null) throw new IllegalArgumentException();
if (invoice.getItems().isEmpty()) throw new IllegalStateException();
}
private double calculateTotal(Invoice invoice) {
// логика расчета
return invoice.getItems().stream()
.mapToDouble(Item::getPrice)
.sum();
}
private void saveInvoice(Invoice invoice, double total) {
// сохранение
}
private void sendConfirmationEmail(Invoice invoice) {
// отправка
}
3. Замена устаревших конструкций (например, switch на полиморфизм)
Часто в legacy коде встречаются большие switch или if-else блоки. Их можно заменить на полиморфизм или паттерн Strategy.
Пример до:
public double calculateShipping(String type, double weight) {
switch (type) {
case "STANDARD":
return weight * 2.0;
case "EXPRESS":
return weight * 5.0;
case "INTERNATIONAL":
return weight * 10.0 + 15;
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
После рефакторинга с использованием паттерна Strategy:
public interface ShippingStrategy {
double calculate(double weight);
}
public class StandardShipping implements ShippingStrategy {
@Override
public double calculate(double weight) {
return weight * 2.0;
}
}
public class ExpressShipping implements ShippingStrategy {
@Override
public double calculate(double weight) {
return weight * 5.0;
}
}
public class InternationalShipping implements ShippingStrategy {
@Override
public double calculate(double weight) {
return weight * 10.0 + 15;
}
}
// Использование
public class ShippingCalculator {
private final Map<String, ShippingStrategy> strategies = new HashMap<>();
public ShippingCalculator() {
strategies.put("STANDARD", new StandardShipping());
strategies.put("EXPRESS", new ExpressShipping());
strategies.put("INTERNATIONAL", new InternationalShipping());
}
public double calculate(String type, double weight) {
ShippingStrategy strategy = strategies.get(type);
if (strategy == null) throw new IllegalArgumentException("Unknown type: " + type);
return strategy.calculate(weight);
}
}
Такой код легче тестировать, расширять и поддерживать.
4. Устранение дублирования (DRY)
В legacy коде часто встречается copy-paste. Найдите общие паттерны и вынесите их в отдельные методы или классы.
Пример:
// В классе UserService
public boolean isEmailValid(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
// В классе OrderService (дублирование)
public boolean isEmailValid(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
После рефакторинга — создаем утилитный класс или используем готовую библиотеку (например, Apache Commons Validator).
Инструменты для рефакторинга Java кода
Современные IDE и инструменты могут сильно упростить рефактори