docs: add ADR-020 and feature docs for MoexClientService split

This commit is contained in:
Sergey Krylov 2026-06-25 20:42:28 +03:00
parent 238836c850
commit d2457d13af
5 changed files with 213 additions and 8 deletions

View File

@ -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)

View File

@ -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`

View File

@ -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-моки

View File

@ -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` успешен

View File

@ -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 как выполненную