Рефакторинг legacy кода на Java: пошаговое руководство
Работа с устаревшим (legacy) кодом на Java — одна из самых частых и сложных задач, с которой сталкиваются разработчики. Часто такой код не имеет тестов, написан в монолитном стиле и содержит множество антипаттернов. Однако полное переписывание системы — это дорого и рискованно. Гораздо эффективнее применить методичный рефакторинг. В этой статье мы разберем практический подход к рефакторингу legacy Java-кода, который позволит сделать его чище, безопаснее и поддерживаемее.
1. Подготовка: анализ и создание "сети безопасности"
Прежде чем менять код, необходимо понять, что он делает и как не сломать существующую логику. Главный принцип рефакторинга legacy кода — не изменять поведение. Для этого нам нужно создать "сеть безопасности" — набор тестов.
Начните с изучения кода. Найдите входные точки (методы, которые вызываются извне) и попробуйте определить ожидаемые результаты. Даже если код написан плохо, он что-то делает правильно.
После анализа напишите характеризующие тесты (Characterization Tests). Это тесты, которые фиксируют текущее поведение кода. Они не проверяют, правильно ли работает логика, а лишь документируют, что происходит сейчас.
Пример: у нас есть класс-монстр, который обрабатывает заказы.
public class OrderProcessor {
public void process(Order order) {
// 200 строк спагетти-кода
if (order.getTotal() > 100) {
order.setStatus("VIP");
} else {
order.setStatus("STANDARD");
}
// ... много другой логики
}
}
Мы не знаем, что внутри, но можем написать тест:
@Test
public void testProcessOrderWithHighTotal() {
// Given
Order order = new Order();
order.setTotal(150);
OrderProcessor processor = new OrderProcessor();
// When
processor.process(order);
// Then (фиксируем текущее поведение)
assertEquals("VIP", order.getStatus());
}
Запустите тесты. Если они проходят — вы зафиксировали текущее поведение. Теперь у вас есть защита от регрессии.
2. Выделение метода (Extract Method)
Самый безопасный и эффективный рефакторинг для больших методов. Суть проста: берем фрагмент кода, который делает что-то одно, и выносим его в отдельный метод с понятным именем.
Рассмотрим пример плохого кода:
public double calculateEmployeePay(Employee emp) {
double basePay = emp.getBasePay();
double overtimePay = 0;
if (emp.getHoursWorked() > 40) {
overtimePay = (emp.getHoursWorked() - 40) * basePay * 1.5;
}
double tax = basePay * 0.13;
double insurance = 5000;
return basePay + overtimePay - tax - insurance;
}
Здесь смешаны расчет переработок, налогов и страховки. Разделим их:
public double calculateEmployeePay(Employee emp) {
double basePay = emp.getBasePay();
double overtimePay = calculateOvertimePay(emp, basePay);
double tax = calculateTax(basePay);
double insurance = getInsuranceCost();
return basePay + overtimePay - tax - insurance;
}
private double calculateOvertimePay(Employee emp, double basePay) {
if (emp.getHoursWorked() > 40) {
return (emp.getHoursWorked() - 40) * basePay * 1.5;
}
return 0;
}
private double calculateTax(double basePay) {
return basePay * 0.13;
}
private double getInsuranceCost() {
return 5000;
}
Теперь каждый метод отвечает за одну задачу. Код стал читаемее, и его легче тестировать по отдельности.
3. Удаление дублирования (DRY)
Legacy код часто страдает от копирования-вставки. Один и тот же кусок логики может встречаться в разных классах. Это опасно: при изменении требований вы можете забыть исправить один из дубликатов.
Предположим, в двух разных сервисах есть код для валидации email:
// В UserService
public boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.length() > 5;
}
// В NotificationService
public boolean checkEmail(String email) {
if (email == null) return false;
return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
Две разные реализации одной и той же бизнес-логики. Решение — вынести в единый утилитарный класс:
public final class EmailValidator {
private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@(.+)$";
private static final Pattern pattern = Pattern.compile(EMAIL_PATTERN);
private EmailValidator() {}
public static boolean isValid(String email) {
if (email == null || email.isBlank()) {
return false;
}
return pattern.matcher(email).matches();
}
}
После этого замените все разрозненные проверки на вызов EmailValidator.isValid(email). Это централизует логику и упростит её изменение в будущем.
4. Разделение классов (Extract Class)
Часто в legacy коде один класс берет на себя слишком много обязанностей (нарушение Single Responsibility Principle). Например, класс ReportGenerator может генерировать отчет, отправлять его по email и сохранять в базу данных. Это три разные ответственности.
Рассмотрим пример:
public class ReportGenerator {
public void generateAndSend(String data) {
// 1. Генерация PDF
String report = generatePdf(data);
// 2. Отправка email
EmailSender sender = new EmailSender();
sender.send("admin@example.com", report);
// 3. Сохранение в БД
saveToDatabase(report);
}
private String generatePdf(String data) {
// логика генерации
return "PDF_" + data;
}
private void saveToDatabase(String report) {
// логика сохранения
}
}
Выделим каждый аспект в отдельный класс:
// Класс для генерации отчета
public class ReportBuilder {
public String build(String data) {
// логика генерации
return "PDF_" + data;
}
}
// Класс для отправки
public class ReportSender {
public void send(String report) {
EmailSender sender = new EmailSender();
sender.send("admin@example.com", report);
}
}
// Класс для хранения
public class ReportRepository {
public void save(String report) {
// логика сохранения
}
}
// Теперь ReportGenerator делегирует задачи
public class ReportGenerator {
private final ReportBuilder builder;
private final Report