surfaces/docs/research/aiogram-architecture-review.md
Mikhail Putilovskij 41660fe84a refactor: rename platform/ → sdk/ to avoid stdlib conflict
platform/ shadowed Python's stdlib platform module, breaking
aiogram/aiohttp/multidict at import time. Renamed to sdk/ and
updated all imports across core/, tests/, and adapter/telegram/.
2026-03-31 21:57:23 +03:00

172 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Ресёрч: aiogram 3.x Architecture Review
> **Дата:** 2026-03-30
> **Вердикт:** APPROVED с двумя уточнениями
---
## 1. Структура проекта
**Официальный пример multi_file_bot:**
```
multi_file_bot/
bot.py
handlers/
common.py
...
```
**Best practice для средних проектов (наш случай):**
```
adapter/telegram/
bot.py ← Dispatcher + include_routers + polling/webhook
converter.py ← граница aiogram ↔ core/
states.py ← все StatesGroup
handlers/ ← по одному Router на модуль
keyboards/ ← InlineKeyboardBuilder фабрики
middleware.py ← DI + logging + rate limit
```
**Оценка:** наша структура соответствует стандарту. ✓
---
## 2. Middleware vs Converter
В aiogram 3.x эти два паттерна решают **разные задачи** и должны использоваться вместе.
| | Middleware | Converter |
|---|---|---|
| Назначение | Infrastructure | Бизнес-логика |
| Что делает | Логирование, DI, rate limit, сессия БД | aiogram Event → IncomingEvent |
| Когда вызывается | До и после хендлера | Внутри хендлера |
**Правильная комбинация:**
```python
# middleware.py — только infrastructure
class DependencyMiddleware(BaseMiddleware):
def __init__(self, platform, store):
self.platform = platform
self.store = store
async def __call__(self, handler, event, data):
data["platform"] = self.platform
data["store"] = self.store
return await handler(event, data)
# handler — converter вызывается внутри
async def handle_message(message: Message, platform, store):
event = to_incoming_message(message) # converter
results = await dispatcher.dispatch(event, platform, store)
await send_results(message, results) # converter обратно
```
**Оценка:** наш converter.py — правильный паттерн. Добавить `middleware.py` для DI. ✓+
---
## 3. Dependency Injection
Стандарт aiogram 3.x — **через middleware + data dict**:
```python
# Регистрация в bot.py
dp.message.middleware(DependencyMiddleware(platform=platform_client, store=store))
# Получение в handler (через type hint на имя ключа)
async def handle_message(message: Message, platform: PlatformClient, store: StateStore):
...
```
Альтернатива — через `dp["key"] = value` (Dispatcher workflow data):
```python
dp["platform"] = platform_client # в bot.py
async def handler(message: Message, platform: PlatformClient): # aiogram сам находит по типу
...
```
**Оценка:** нужно явно добавить один из этих механизмов, иначе хендлеры не получат platform/store. ⚠️
---
## 4. InlineKeyboardBuilder
`InlineKeyboardBuilder` — рекомендуемый подход в aiogram 3.x. `InlineKeyboardMarkup` с вложенными списками считается устаревшим стилем.
```python
# keyboards/chat.py
from aiogram.utils.keyboard import InlineKeyboardBuilder
def chats_keyboard(chats: list[ChatContext]) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for chat in chats:
builder.button(text=f"💬 {chat.name}", callback_data=f"chat:{chat.chat_id}")
builder.button(text=" Новый чат", callback_data="new_chat")
builder.adjust(1) # одна кнопка в строку
return builder.as_markup()
```
**Оценка:** использовать `InlineKeyboardBuilder` везде. ✓
---
## 5. F-фильтры (MagicFilter)
aiogram 3.x MagicFilter (`F`) — стандарт вместо ручных проверок в хендлерах:
```python
from aiogram import F
# Вместо if message.text == "/start" внутри хендлера
router.message.register(start_handler, Command("start"))
# Фильтр по типу вложения
router.message.register(voice_handler, F.voice)
router.message.register(photo_handler, F.photo)
# Фильтр по состоянию
router.message.register(handle_name_input, OnboardingState.waiting_for_name)
# Callback фильтр
router.callback_query.register(confirm_handler, F.data.startswith("confirm:"))
```
**Оценка:** использовать F-фильтры при регистрации роутеров — чище, чем if/else в хендлерах. ✓
---
## 6. Сцены (Scenes) — новинка aiogram 3.x
aiogram 3.4+ ввёл `Scene` как улучшенный FSM для сложных диалогов:
```python
from aiogram.fsm.scene import Scene, on
class OnboardingScene(Scene, state="onboarding"):
@on.message.enter()
async def on_enter(self, message: Message):
await message.answer("Как зовут твоего агента?")
@on.message()
async def on_name(self, message: Message, state: FSMContext):
await state.update_data(agent_name=message.text)
await self.wizard.goto(OnboardingScene2)
```
**Оценка:** Scenes — опциональное улучшение для онбординга. Классический FSM через StatesGroup тоже корректен и проще для понимания. Использовать StatesGroup для прототипа, Scenes — в будущем. ✓
---
## Итог
| Решение | Статус |
|---|---|
| Router-based архитектура, один Router на модуль | ✅ Стандарт |
| converter.py как граница aiogram ↔ core/ | ✅ Правильный паттерн |
| InlineKeyboardBuilder в keyboards/ | ✅ Рекомендуется |
| SQLiteStorage для FSM | ✅ Стандарт для MVP |
| **Нужно добавить: DependencyMiddleware** | ⚠️ DI без него не работает |
| **Нужно добавить: F-фильтры при регистрации** | ⚠️ Иначе проверки в хендлерах |
**Архитектура одобрена.** Два уточнения (middleware.py и F-фильтры) небольшие и органично вписываются в текущую структуру.