From d2457d13afd971bd3eb45b6529e5026c29bc1191 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 25 Jun 2026 20:42:28 +0300 Subject: [PATCH] docs: add ADR-020 and feature docs for MoexClientService split --- .../docs/adr/ADR-020-moex-client-split.md | 63 ++++++++++++++++ .../tasks.md | 11 +-- docs/features/moex-client-split/plan.md | 73 +++++++++++++++++++ docs/features/moex-client-split/spec.md | 35 +++++++++ docs/features/moex-client-split/tasks.md | 39 ++++++++++ 5 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 apps/docs/docs/adr/ADR-020-moex-client-split.md create mode 100644 docs/features/moex-client-split/plan.md create mode 100644 docs/features/moex-client-split/spec.md create mode 100644 docs/features/moex-client-split/tasks.md diff --git a/apps/docs/docs/adr/ADR-020-moex-client-split.md b/apps/docs/docs/adr/ADR-020-moex-client-split.md new file mode 100644 index 0000000..f6fd330 --- /dev/null +++ b/apps/docs/docs/adr/ADR-020-moex-client-split.md @@ -0,0 +1,63 @@ +# ADR-020: Разделение MoexClientService на доменные клиенты + +**Дата:** 2026-06-25 +**Статус:** Принято +**Автор:** AI Agent (codex/backend-architecture-improvements) + +## Контекст + +`MoexClientService` в `apps/backend/src/modules/moex-client/moex-client.service.ts` (423 строки, 11 публичных методов) со временем стал God Service: + +- Нарушает SRP — содержит логику работы с акциями, облигациями, свечами, историей, дивидендами и поиском в одном классе +- Инфраструктура (rate limiter, circuit breaker) смешана с бизнес-логикой +- Все 11 методов используют один шаблон: `request()` → `extractTable()` → map, но каждый с разными endpoint-ами и типами +- Потребители (shares, bonds, candles, portfolio, securities, screener, tbank/events) получают весь сервис целиком, а не только нужную функциональность +- Тестирование затруднено: любой тест одного метода тянет весь сервис + +## Рассмотренные варианты + +### A. Полный сплит по доменам (выбран) + +Выделить инфраструктурный слой (`MoexHttpClient`) и 5 доменных клиентов — по одному на группу MOEX-запросов. + +### B. Минимальный сплит + +Вынести только `MoexHttpClient` (request + extractTable + circuit breaker + rate limiter), оставить все методы в одном сервисе с делегированием через DI. + +Отклонён: не решает проблему God Service — один сервис всё ещё содержит всю доменную логику. + +### C. Оставить как есть + +Отклонён: противоречит результатам аудита, 423-строчный сервис ухудшает поддерживаемость. + +## Решение + +Выбран вариант A — полный сплит по доменам: + +- `MoexHttpClient` — инфраструктура (axios, PQueue, circuit breaker, `request()`, `extractTable()`) +- `MoexSecuritiesClient` — `searchSecurities()`, `getSecurityDescription()` +- `MoexMarketDataClient` — `getShareMarketData()`, `getShareMarketDataBatch()`, `getBondData()`, `getBondMarketData()`, `getBondPositionDataBatch()` +- `MoexCandlesClient` — `getCandles()` +- `MoexHistoryClient` — `getHistory()`, `getBondHistory()` +- `MoexDividendsClient` — `getDividends()` + +Модуль теряет `@Global()` — каждый потребитель явно импортирует `MoexClientModule`. + +## Последствия + +### Положительные +- Чёткое разделение ответственности — каждый клиент отвечает за один домен MOEX API +- Возможность мокать только нужный клиент в тестах потребителей +- `MoexHttpClient` — внутренняя деталь, не экспортируется из модуля +- Явные зависимости через imports модулей вместо одного God Service + +### Риски +- Миграция всех 7 потребителей в одном коммите (нельзя оставить половинчатое состояние) +- Каждый потребитель должен импортировать `MoexClientModule` — больше boilerplate +- Необходимость обновить все тесты потребителей (DI-инъекция меняется) + +## Связанные документы +- ADR-003: Стратегия rate limiting (остаётся актуальной, инфраструктура переносится в MoexHttpClient) +- ADR-004: Feature modules (принцип явных зависимостей) +- `docs/features/backend-architecture-improvements/plan.md` (Iteration 6) +- `docs/features/backend-architecture-improvements/tasks.md` (Iteration 6) diff --git a/docs/features/backend-architecture-improvements/tasks.md b/docs/features/backend-architecture-improvements/tasks.md index 42a5194..4bfcd24 100644 --- a/docs/features/backend-architecture-improvements/tasks.md +++ b/docs/features/backend-architecture-improvements/tasks.md @@ -45,12 +45,7 @@ - [x] 5.2 Убран `new RequestLoggingMiddleware()` и `app.use()` из `main.ts` - [x] 120 тестов проходят, build успешен -## Итерация 6: MoexClientService split (отдельный эпик) +## Итерация 6: MoexClientService split → `docs/features/moex-client-split/` -- [ ] 6.1 ADR на разделение MoexClientService -- [ ] 6.2 spec/plan/tasks отдельного эпика -- [ ] 6.3 Выделение rate limiter + circuit breaker в shared utils -- [ ] 6.4 Создание MoexSecuritiesClient -- [ ] 6.5 Создание MoexMarketDataClient -- [ ] 6.6 Создание MoexCandlesClient -- [ ] 6.7 Обновление всех потребителей +- [x] 6.1 ADR на разделение MoexClientService +- [x] 6.2 spec/plan/tasks отдельного эпика → перенесено в `docs/features/moex-client-split/{spec,plan,tasks}.md` diff --git a/docs/features/moex-client-split/plan.md b/docs/features/moex-client-split/plan.md new file mode 100644 index 0000000..4a468d0 --- /dev/null +++ b/docs/features/moex-client-split/plan.md @@ -0,0 +1,73 @@ +# MoexClientService Split — Plan + +## Подход + +Полный сплит по доменам (ADR-020). Один коммит — миграция всех файлов одновременно. + +## Архитектура + +``` +modules/moex-client/ +├── moex-http.client.ts # Инфраструктура: axios, PQueue, circuit breaker +├── moex-securities.client.ts # searchSecurities(), getSecurityDescription() +├── moex-market-data.client.ts # getShareMarketData(), getShareMarketDataBatch(), +│ # getBondData(), getBondMarketData(), getBondPositionDataBatch() +├── moex-candles.client.ts # getCandles() +├── moex-history.client.ts # getHistory(), getBondHistory() +├── moex-dividends.client.ts # getDividends() +├── moex-client.types.ts # (unchanged) +├── moex-client.module.ts # providers: все клиенты, exports: доменные клиенты, не @Global() +├── moex-client.service.spec.ts # → moex-http.client.spec.ts +└── moex-client.service.integration.spec.ts # → интеграционные тесты +``` + +## Data Flow + +``` +Consumer Service + ↓ (DI) +Domain Client (MoexSecuritiesClient | MoexMarketDataClient | etc.) + ↓ (DI) +MoexHttpClient (request + extractTable) + ↓ +MOEX ISS API +``` + +`MoexHttpClient` — не экспортируется из модуля, только доменные клиенты его видят. + +## Consumer Updates + +| Модуль | Было | Стало | +|--------|------|-------| +| `shares/shares.module.ts` | — | `imports: [MoexClientModule]` | +| `shares/shares.service.ts` | `moexClient: MoexClientService` | `moexSecurities: MoexSecuritiesClient`, `moexMarketData: MoexMarketDataClient` | +| `bonds/bonds.module.ts` | — | `imports: [MoexClientModule]` | +| `bonds/bonds.service.ts` | `moexClient: MoexClientService` | `moexMarketData: MoexMarketDataClient`, `moexHistory: MoexHistoryClient` | +| `candles/candles.module.ts` | — | `imports: [MoexClientModule]` | +| `candles/candles.service.ts` | `moexClient: MoexClientService` | `moexCandles: MoexCandlesClient` | +| `securities/securities.module.ts` | — | `imports: [MoexClientModule]` | +| `securities/securities.service.ts` | `moexClient: MoexClientService` | `moexSecurities: MoexSecuritiesClient`, `moexMarketData: MoexMarketDataClient` | +| `securities/screener.service.ts` | `moexClient: MoexClientService` | `moexMarketData: MoexMarketDataClient` | +| `portfolio/portfolio.module.ts` | — | `imports: [MoexClientModule]` | +| `portfolio/portfolio.service.ts` | `moexClient: MoexClientService` | `moexSecurities: MoexSecuritiesClient`, `moexMarketData: MoexMarketDataClient`, `moexDividends: MoexDividendsClient` | +| `tbank/tbank.module.ts` | `imports: [MoexClientModule]` | (unchanged) | +| `tbank/.../broker-events.service.ts` | `moexClient: MoexClientService` | `moexDividends: MoexDividendsClient`, `moexMarketData: MoexMarketDataClient` | + +## Migration Order + +1. Создать `MoexHttpClient` — перенести инфраструктуру из `MoexClientService` +2. Создать `MoexSecuritiesClient` — перенести 2 метода +3. Создать `MoexMarketDataClient` — перенести 5 методов +4. Создать `MoexCandlesClient` — перенести 1 метод +5. Создать `MoexHistoryClient` — перенести 2 метода +6. Создать `MoexDividendsClient` — перенести 1 метод +7. Обновить `MoexClientModule` — убрать `@Global()`, новые providers/exports +8. Обновить всех потребителей (модули + сервисы + тесты) +9. Удалить старый `MoexClientService` +10. `npm run build && npm run test` + +## Testing Strategy + +- `MoexHttpClient` — unit-тесты на circuit breaker, rate limiter, request +- Каждый доменный клиент — unit-тесты с mocked `MoexHttpClient` +- Существующие тесты потребителей — обновить DI-моки diff --git a/docs/features/moex-client-split/spec.md b/docs/features/moex-client-split/spec.md new file mode 100644 index 0000000..5b5fb1b --- /dev/null +++ b/docs/features/moex-client-split/spec.md @@ -0,0 +1,35 @@ +# MoexClientService Split + +## Цель + +Разделить God Service `MoexClientService` на инфраструктурный слой и набор доменных клиентов, устранив нарушение SRP и улучшив тестируемость. + +## Требования + +1. `MoexHttpClient` — выделить инфраструктуру (axios, PQueue, circuit breaker, `request()`, `extractTable()`) +2. Доменные клиенты — по одному на группу MOEX-запросов: + - `MoexSecuritiesClient` — поиск и описание ценных бумаг + - `MoexMarketDataClient` — рыночные данные акций и облигаций + - `MoexCandlesClient` — свечи + - `MoexHistoryClient` — история торгов + - `MoexDividendsClient` — дивиденды +3. Убрать `@Global()` — каждый потребитель явно импортирует `MoexClientModule` +4. `MoexHttpClient` не экспортируется из модуля (внутренняя деталь) +5. Все MOEX-типы остаются в `moex-client.types.ts` +6. API-контракт всех потребителей не меняется — только DI + +## Ограничения + +- Один коммит на всю миграцию (нельзя половинчатое состояние) +- Не менять сигнатуры публичных методов — только перенос кода +- Не менять типы в `moex-client.types.ts` +- Каждое изменение через TDD-цикл + +## Критерии приемки (Acceptance Criteria) + +- [ ] `MoexClientService` удалён, все 11 методов распределены по 5 доменным клиентам +- [ ] `MoexHttpClient` содержит rate limiter + circuit breaker +- [ ] `@Global()` убран с `MoexClientModule` +- [ ] Все 7 потребителей обновлены: явный импорт модуля + новые DI +- [ ] Все тесты проходят (существующие обновлены) +- [ ] `npm run build` успешен diff --git a/docs/features/moex-client-split/tasks.md b/docs/features/moex-client-split/tasks.md new file mode 100644 index 0000000..75190b8 --- /dev/null +++ b/docs/features/moex-client-split/tasks.md @@ -0,0 +1,39 @@ +# MoexClientService Split — Tasks + +## Этап 1: Создание инфраструктурного клиента + +- [ ] 1.1 Создать `moex-http.client.ts` — перенести `request()`, `extractTable()`, circuit breaker, PQueue из `MoexClientService` +- [ ] 1.2 Написать unit-тесты для `MoexHttpClient` +- [ ] 1.3 Написать тест на circuit breaker (threshold → open → reset) + +## Этап 2: Создание доменных клиентов + +- [ ] 2.1 `MoexSecuritiesClient` — `searchSecurities()`, `getSecurityDescription()` +- [ ] 2.2 `MoexMarketDataClient` — `getShareMarketData()`, `getShareMarketDataBatch()`, `getBondData()`, `getBondMarketData()`, `getBondPositionDataBatch()` +- [ ] 2.3 `MoexCandlesClient` — `getCandles()` +- [ ] 2.4 `MoexHistoryClient` — `getHistory()`, `getBondHistory()` +- [ ] 2.5 `MoexDividendsClient` — `getDividends()` +- [ ] 2.6 Написать unit-тесты для каждого доменного клиента (mocked http client) + +## Этап 3: Обновление модуля + +- [ ] 3.1 Убрать `@Global()` из `MoexClientModule` +- [ ] 3.2 Добавить новые клиенты в providers/exports +- [ ] 3.3 Убрать старый `MoexClientService` из providers +- [ ] 3.4 Проверить, что `MoexHttpClient` не экспортируется + +## Этап 4: Миграция потребителей + +- [ ] 4.1 `shares/` — обновить модуль, сервис, тесты +- [ ] 4.2 `bonds/` — обновить модуль, сервис, тесты +- [ ] 4.3 `candles/` — обновить модуль, сервис, тесты +- [ ] 4.4 `securities/` — обновить модуль, securities.service, screener.service, тесты +- [ ] 4.5 `portfolio/` — обновить модуль, сервис, тесты +- [ ] 4.6 `tbank/` — обновить broker-events.service, тесты + +## Этап 5: Финализация + +- [ ] 5.1 Удалить старый `moex-client.service.ts` +- [ ] 5.2 `npm run build` успешен +- [ ] 5.3 Все тесты проходят +- [ ] 5.4 Обновить `docs/features/backend-architecture-improvements/tasks.md` — отметить Iteration 6 как выполненную