Рефакторинг7 мин чтения2026-03-06

Java рефакторинг: как привести код в порядок, не сломав функциональность

Я начал писать код на Java в 2014 году. Помню первый проект в стартапе — кодовая база из 50 тысяч строк, которую касалось человек пять. Каждый добавлял своё, пе

Я начал писать код на Java в 2014 году. Помню первый проект в стартапе — кодовая база из 50 тысяч строк, которую касалось человек пять. Каждый добавлял своё, переписывал чужое, и через пару лет это превратилось в кошмар. Тогда я понял: код пишется один раз, а читается и меняется сто раз. И если его не поддерживать, он быстро становится неподъёмным грузом для всей команды.

Рефакторинг — это не переписывание с нуля. Это хирургия, а не реконструкция. Меняешь внутренность, снаружи всё остаётся как было. Функциональность не меняется, тесты должны проходить, пользователь ничего не замечает. Зато разработчикам становится легче дышать.

Что такое рефакторинг и почему он нужен

Если честно, слово "рефакторинг" часто произносят в контексте "переписать всё заново". Это не про то. Рефакторинг — это улучшение структуры существующего кода без изменения его поведения.

Представь: у тебя есть метод из 200 строк, который делает пять разных вещей. Он работает, тесты зелёные, но когда приходит новичок и пытается его понять, он теряет час. Потом нужно добавить функцию — и оказывается, что она зарыта в глубине этого метода, извлечь её сложно. Вот тут рефакторинг спасает.

По моему опыту, команды, которые рефакторят регулярно — как часть обычного процесса, — двигаются быстрее. Не на 10%, а на 30-50%. Меньше багов, проще онбординг новичков, приятнее работать.

Когда он нужен? Когда код начинает работать против тебя:

Если хотя бы два пункта про твой проект — пора действовать.

Code smells: как распознать проблемный код

Code smell — не баг, не ошибка. Это запах, намёк на проблему. Код работает, но что-то не то. Мартин Фаулер в своей книге "Рефакторинг" выделил больше двадцати таких запахов. Расскажу про самые частые, которые встречаю постоянно.

Длинный метод. Если метод больше 30-50 строк, это уже подозрительно. Если больше 100 — это точно проблема.

// Плохо
public void processUserOrder(User user, List<Item> items, 
                             Address address, Payment payment) {
    // Валидация пользователя
    if (user == null) throw new IllegalArgumentException("User is null");
    if (user.getId() <= 0) throw new IllegalArgumentException("Invalid user ID");
    
    // Валидация товаров
    if (items == null || items.isEmpty()) 
        throw new IllegalArgumentException("Items is empty");
    for (Item item : items) {
        if (item.getPrice() < 0) 
            throw new IllegalArgumentException("Invalid price");
    }
    
    // Валидация адреса
    if (address == null || address.getZipCode() == null) 
        throw new IllegalArgumentException("Invalid address");
    
    // Расчёт стоимости
    double total = 0;
    for (Item item : items) {
        total += item.getPrice() * item.getQuantity();
    }
    
    // Применение скидок
    if (user.isVip()) {
        total *= 0.9;
    }
    if (total > 1000) {
        total *= 0.95;
    }
    
    // Обработка платежа
    if (payment.getAmount() < total) {
        throw new IllegalArgumentException("Insufficient funds");
    }
    
    // Сохранение заказа
    Order order = new Order();
    order.setUser(user);
    order.setItems(items);
    order.setTotal(total);
    order.setAddress(address);
    orderRepository.save(order);
    
    // Отправка уведомления
    emailService.sendOrderConfirmation(user, order);
    smsService.sendOrderConfirmation(user, order);
}

200 строк в одном методе. Он валидирует, считает, сохраняет, отправляет уведомления. Если тебе нужно изменить логику скидок — ты вынужден трогать весь метод. Если в валидации ошибка — нужно перелопачивать всю функцию.

Как это выглядит после рефакторинга?

// Хорошо
public void processUserOrder(User user, List<Item> items, 
                             Address address, Payment payment) {
    userValidator.validate(user);
    itemValidator.validate(items);
    addressValidator.validate(address);
    
    OrderCalculation calculation = orderCalculator.calculate(user, items);
    paymentValidator.validate(payment, calculation.getTotal());
    
    Order order = orderFactory.createOrder(user, items, address, calculation);
    orderRepository.save(order);
    
    notificationService.notifyOrderConfirmed(user, order);
}

Тот же результат, но теперь каждый кусок отвечает за свою задачу. Хочешь поменять скидки? Правишь orderCalculator. Новая валидация? Добавляешь в userValidator. Просто. Понятно. Тестируется отдельно.

Дублирование кода. Если один и тот же код копируешь в третий раз — извлеки его в метод. Я видел базы с одинаковыми блоками валидации в пяти разных местах. Потом обнаруживалась ошибка в валидации, и нужно было пятеро раз её чинить.

Слишком много параметров в методе. Если методу нужно больше 4-5 параметров, это запах. Значит, он делает слишком много или ты неправильно структурировал данные.

// Плохо
public Invoice createInvoice(String clientName, String clientEmail, 
                            String address, String zipCode, String city,
                            List<Item> items, double discount, 
                            Date dueDate, String notes) {
    // ...
}

// Хорошо
public Invoice createInvoice(Client client, InvoiceItems items, 
                            InvoiceSettings settings) {
    // ...
}

Класс делает слишком много. God object. Класс на 2000 строк, который отвечает за всё: логику бизнеса, валидацию, сохранение, отправку писем. Такой класс невозможно тестировать, он меняется чуть ли не каждый день.

Циклическая сложность. Когда в методе 15 if-else вложенных друг в друга. Читать такое — пытка. Обычно это решается через полиморфизм или Strategy pattern.

// Плохо
public double calculatePrice(Order order) {
    double price = order.getBasePrice();
    if (order.getCustomer().isVip()) {
        if (order.getTotal() > 1000) {
            if (order.isHoliday()) {
                price *= 0.7;
            } else {
                price *= 0.75;
            }
        } else {
            price *= 0.9;
        }
    } else {
        if (order.isHoliday()) {
            price *= 0.95;
        }
    }
    return price;
}

// Хорошо
public double calculatePrice(Order order) {
    PricingStrategy strategy = strategyFactory.getStrategy(order);
    return strategy.apply(order.getBasePrice());
}

Методы рефакторинга: от простого к сложному

Есть десятки техник. Расскажу про самые полезные, которые используешь постоянно.

Extract Method. Самая частая операция. Выделяешь кусок кода в отдельный метод.

// До
public void generateReport(List<Sale> sales) {
    System.out.println("=== SALES REPORT ===");
    double total = 0;
    for (Sale sale : sales) {
        if (sale.getDate().isAfter(LocalDate.now().minusMonths(1))) {
            total += sale.getAmount();
            System.out.println(sale.getProductName() + ": " + sale.getAmount());
        }
    }
    System.out.println("Total: " + total);
}

// После
public void generateReport(List<Sale> sales) {
    List<Sale> recentSales = filterRecentSales(sales);
    double total = calculateTotal(recentSales);
    printReport(recentSales, total);
}

private List<Sale> filterRecentSales(List<Sale> sales) {
    return sales.stream()
        .filter(s -> s.getDate().isAfter(LocalDate.now().minusMonths(1)))
        .collect(Collectors.toList());
}

private double calculateTotal(List<Sale> sales) {
    return sales.stream()
        .mapToDouble(Sale::getAmount)
        .sum();
}

private void printReport(List<Sale> sales, double total) {
    System.out.println("=== SALES REPORT ===");
    sales.forEach(s -> System.out.println(s.getProductName() + ": " + s.getAmount()));
    System.out.println("Total: " + total);
}

Rename. Переименовываешь переменные, методы, классы на более понятные. Звучит мелко, но это огромное влияние на читаемость.

// До
private void proc(List<User> u) {
    for (User x : u) {
        if (x.getA() > 18 && x.getB().equals("ACTIVE")) {
            s.send(x);
        }
    }
}

// После
private void sendNotificationsToActiveAdults(List<User> users) {
    for (User user : users) {
        if (user.getAge() > 18 && user.getStatus().equals("ACTIVE")) {
            notificationService.send(user);
        }
    }
}

Replace Magic Numbers with Constants. Волшебные числа разбросаны по коду — что это вообще? 18? 1000? 86400?

// До
if (user.getAge() > 18 && order.getTotal() > 1000 && 
    (System.currentTimeMillis() - lastPurchase) > 86400000) {
    // ...
}

// После
private static final int ADULT_AGE = 18;
private static final double PREMIUM_THRESHOLD = 1000.0;
private static final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000;

if (user.getAge() > ADULT_AGE && 
    order.getTotal() > PREMIUM_THRESHOLD && 
    (System.currentTimeMillis() - lastPurchase) > ONE_DAY_MILLIS) {
    // ...
}

Introduce Parameter Object. Когда в методе пять параметров с одной темой, объедини их.

// До
public Invoice createInvoice(String street, String city, 
                            String zipCode, String country) {
    // ...
}

// После
public class Address {
    private String street;
    private String city;
    private String zipCode;
    private String country;
}

public Invoice createInvoice(Address address) {
    // ...
}

Replace Temp with Query. Временные переменные часто мешают. Замени вычисление на метод.

// До
public double getPrice() {
    int basePrice = quantity * itemPrice;
    if (basePrice > 1000)
        return basePrice * 0.95;
    return basePrice;
}

// После
public double getPrice() {
    if (getBasePrice() > 1000)
        return getBasePrice() * 0.95;
    return getBasePrice();
}

private int getBasePrice() {
    return quantity * itemPrice;
}

Как рефакторить, не сломав всё

Вот тут главное — дисциплина. Я видел, как команды начинали рефакторинг с лучшими намерениями, а заканчивали откатом всего коммита через неделю борьбы с багами.

Первое правило: тесты. Если тестов нет — сначала напиши их. Это подушка безопасности.

// Пример: есть код, нет тестов
public class PriceCalculator {
    public double calculatePrice(Order order) {
        double price = order.getBasePrice();
        if (order.getCustomer().isVip()) {
            price *= 0.9;
        }
        return price;
    }
}

// Добавляем тесты перед рефакторингом
@Test
public void testVipDiscount() {
    Customer vipCustomer = new Customer(true);
    Order order = new Order(100.0, vipCustomer);
    
    PriceCalculator calculator = new PriceCalculator();
    assertEquals(90.0, calculator.calculatePrice(order), 0.01);
}

@Test
public void testRegularCustomer() {
    Customer regularCustomer = new Customer(false);
    Order order = new Order(100.0, regularCustomer);
    
    PriceCalculator calculator = new PriceCalculator();
    assertEquals(100.0, calculator.calculatePrice(order), 0.01);
}

Теперь тесты проходят. Теперь можешь рефакторить. Если что-то сломается — тесты сразу скажут.

Второе правило: маленькие шаги. Не переписывай класс полностью. Сначала extract method, потом переименуй, потом спрячь детали реализации. Каждый шаг — коммит, запуск тестов, проверка.

Я вижу в практике: когда разработчик делает огромный рефакторинг в одном коммите, code review становится невозможным. Никто не может разобраться, что изменилось. А когда по маленьким кусочкам — легко следить.

**Трет

Попробуйте Distiq для автоматического code review

AI-бот анализирует каждый MR/PR и оставляет комментарии с замечаниями. Интеграция за 2 минуты.

Попробовать бесплатно

Похожие статьи