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