Files
marketing-parser/Техническое задание для AI-агента: Рефакторинг ParserController.md
T
2025-09-14 18:10:45 +05:00

9.5 KiB

Техническое задание для AI-агента: Рефакторинг ParserController

Задача: Провести рефакторинг ParserController и связанных сервисов, чтобы сделать код более чистым, масштабируемым и простым в поддержке.

Проблемы текущей реализации:

  1. Дублирование кода: Методы parse...RssFeed() практически идентичны.
  2. Нарушение SRP: Контроллер отвечает за запуск парсеров, получение данных, статистику и проверку состояния.
  3. Низкая масштабируемость: Добавление нового парсера требует изменения контроллера в 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 на несколько маленьких

Разделите один большой контроллер на три, каждый со своей зоной ответственности:

  1. MarketItemController — для публичных запросов на получение данных.
  2. ParserAdminController — для административных действий (запуск парсеров).
  3. 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. Контроллеры менять не нужно.
  • Эффективность: Запуск всех парсеров теперь может выполняться параллельно.