9.5 KiB
Техническое задание для AI-агента: Рефакторинг ParserController
Задача: Провести рефакторинг ParserController и связанных сервисов, чтобы сделать код более чистым, масштабируемым и простым в поддержке.
Проблемы текущей реализации:
- Дублирование кода: Методы
parse...RssFeed()практически идентичны. - Нарушение SRP: Контроллер отвечает за запуск парсеров, получение данных, статистику и проверку состояния.
- Низкая масштабируемость: Добавление нового парсера требует изменения контроллера в 3-4 местах (добавление зависимости, нового эндпоинта, обновление метода
parseAll, обновлениеscheduler/info).
План рефакторинга
Шаг 1: Создание общего интерфейса ParserService (Паттерн "Стратегия")
Создайте общий интерфейс, который будут реализовывать все парсеры. Это позволит нам работать с ними единообразно.
public interface ParserService {
/**
* Возвращает уникальное имя источника (например, "kursiv", "kapital").
* @return String source name
*/
String getSourceName();
/**
* Запускает парсинг и сохранение данных для своего источника.
* @return List of newly saved MarketItem
*/
List<MarketItem> parseAndSaveRssFeed();
}
Шаг 2: Модификация существующих сервисов
Каждый из ваших сервисов (KursivParserService, KapitalParserService и т.д.) должен реализовать этот интерфейс.
Пример для KursivParserService:
@Service
public class KursivParserService implements ParserService {
@Override
public String getSourceName() {
return "kursiv"; // Уникальное имя в нижнем регистре
}
@Override
public List<MarketItem> parseAndSaveRssFeed() {
// ... существующая логика парсинга для Kursiv ...
}
}
Проделайте это для всех 5 парсер-сервисов.
Шаг 3: Создание ParserManagerService (Паттерн "Фасад" / Service Locator)
Создайте новый сервис, который будет управлять всеми парсерами. Spring Boot автоматически соберет все бины, реализующие ParserService, в один список.
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.springframework.scheduling.annotation.Async;
@Service
public class ParserManagerService {
private final Map<String, ParserService> parsers;
// Spring автоматически инжектирует все бины типа ParserService
public ParserManagerService(List<ParserService> parserServices) {
this.parsers = parserServices.stream()
.collect(Collectors.toMap(ParserService::getSourceName, service -> service));
}
/**
* Запускает парсер по его имени.
* @param sourceName Имя источника (например, "kursiv")
* @return Результат парсинга
*/
public List<MarketItem> runParser(String sourceName) {
ParserService parser = Optional.ofNullable(parsers.get(sourceName))
.orElseThrow(() -> new IllegalArgumentException("Парсер не найден: " + sourceName));
return parser.parseAndSaveRssFeed();
}
/**
* Асинхронно запускает все парсеры.
* @return Список результатов для каждого парсера
*/
@Async // Для параллельного выполнения
public CompletableFuture<List<ParserResultDto>> runAllParsers() {
long totalItemsBefore = marketItemService.getTotalItemsCount(); // Предполагая, что у вас есть доступ к этому сервису
List<ParserResultDto> results = parsers.values().parallelStream()
.map(parser -> {
List<MarketItem> savedItems = parser.parseAndSaveRssFeed();
return new ParserResultDto(parser.getSourceName(), savedItems.size(), totalItemsBefore + savedItems.size(), "completed");
})
.collect(Collectors.toList());
return CompletableFuture.completedFuture(results);
}
public List<String> getAvailableParsers() {
return parsers.keySet().stream().sorted().collect(Collectors.toList());
}
}
Не забудьте добавить @EnableAsync в главный класс вашего приложения.
Шаг 4: Разделение ParserController на несколько маленьких
Разделите один большой контроллер на три, каждый со своей зоной ответственности:
MarketItemController— для публичных запросов на получение данных.ParserAdminController— для административных действий (запуск парсеров).HealthCheckController— для эндпоинтов мониторинга.
Шаг 5: Реализация новых контроллеров
1. MarketItemController.java
(Содержит эндпоинты, которые нужны фронтенду для отображения данных)
@RestController
@RequestMapping("/api/parser/items")
public class MarketItemController {
@Autowired private MarketItemService marketItemService;
@GetMapping
public ResponseEntity<ApiResponse<Page<MarketItem>>> getItems(...) { ... }
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<MarketItem>> getItemById(@PathVariable String id) { ... }
@GetMapping("/stats")
public ResponseEntity<ApiResponse<ParserStatsDto>> getStats() { ... }
}
2. ParserAdminController.java
(Содержит эндпоинты для управления парсерами, возможно, их стоит защитить в будущем)
@RestController
@RequestMapping("/api/parser/admin/parsers")
public class ParserAdminController {
@Autowired private ParserManagerService parserManagerService;
@Autowired private MarketItemService marketItemService; // для подсчета totalItems
// Один динамический эндпоинт вместо пяти
@PostMapping("/parse/{sourceName}")
public ResponseEntity<ApiResponse<ParserResultDto>> parseSource(@PathVariable String sourceName) {
try {
List<MarketItem> savedItems = parserManagerService.runParser(sourceName);
long totalItems = marketItemService.getTotalItemsCount();
ParserResultDto result = new ParserResultDto(sourceName, savedItems.size(), totalItems, "completed");
return ResponseEntity.ok(ApiResponse.success("Парсинг " + sourceName + " завершен", result));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(ApiResponse.error(e.getMessage()));
}
}
// Упрощенный и асинхронный метод
@PostMapping("/parse/all")
public ResponseEntity<ApiResponse<List<ParserResultDto>>> parseAll() {
try {
List<ParserResultDto> results = parserManagerService.runAllParsers().get(); // .get() для ожидания результата
return ResponseEntity.ok(ApiResponse.success("Парсинг всех источников запущен", results));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(ApiResponse.error(e.getMessage()));
}
}
}
3. HealthCheckController.java
(Содержит все /health и информационные эндпоинты)
@RestController
@RequestMapping("/api/parser/health")
public class HealthCheckController {
// ... методы healthCheck(), checkMongoConnection(), getSchedulerInfo() ...
// Метод getSchedulerInfo можно улучшить, получая список парсеров из ParserManagerService
}
Преимущества нового подхода
- DRY (Don't Repeat Yourself): Убрано дублирование кода в эндпоинтах.
- SRP (Single Responsibility Principle): Каждый контроллер отвечает за свою область.
- Масштабируемость: Чтобы добавить новый парсер, достаточно создать новый сервис, реализующий
ParserService. Контроллеры менять не нужно. - Эффективность: Запуск всех парсеров теперь может выполняться параллельно.