codex/backend-architecture-improvements #47
63
apps/docs/docs/adr/ADR-020-moex-client-split.md
Normal file
63
apps/docs/docs/adr/ADR-020-moex-client-split.md
Normal 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)
|
||||||
@ -45,12 +45,7 @@
|
|||||||
- [x] 5.2 Убран `new RequestLoggingMiddleware()` и `app.use()` из `main.ts`
|
- [x] 5.2 Убран `new RequestLoggingMiddleware()` и `app.use()` из `main.ts`
|
||||||
- [x] 120 тестов проходят, build успешен
|
- [x] 120 тестов проходят, build успешен
|
||||||
|
|
||||||
## Итерация 6: MoexClientService split (отдельный эпик)
|
## Итерация 6: MoexClientService split → `docs/features/moex-client-split/`
|
||||||
|
|
||||||
- [ ] 6.1 ADR на разделение MoexClientService
|
- [x] 6.1 ADR на разделение MoexClientService
|
||||||
- [ ] 6.2 spec/plan/tasks отдельного эпика
|
- [x] 6.2 spec/plan/tasks отдельного эпика → перенесено в `docs/features/moex-client-split/{spec,plan,tasks}.md`
|
||||||
- [ ] 6.3 Выделение rate limiter + circuit breaker в shared utils
|
|
||||||
- [ ] 6.4 Создание MoexSecuritiesClient
|
|
||||||
- [ ] 6.5 Создание MoexMarketDataClient
|
|
||||||
- [ ] 6.6 Создание MoexCandlesClient
|
|
||||||
- [ ] 6.7 Обновление всех потребителей
|
|
||||||
|
|||||||
73
docs/features/moex-client-split/plan.md
Normal file
73
docs/features/moex-client-split/plan.md
Normal 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-моки
|
||||||
35
docs/features/moex-client-split/spec.md
Normal file
35
docs/features/moex-client-split/spec.md
Normal 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` успешен
|
||||||
39
docs/features/moex-client-split/tasks.md
Normal file
39
docs/features/moex-client-split/tasks.md
Normal 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 как выполненную
|
||||||
Loading…
x
Reference in New Issue
Block a user