Я начал писать код на 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 становится невозможным. Никто не может разобраться, что изменилось. А когда по маленьким кусочкам — легко следить.
**Трет
