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

6.4 KiB
Raw Blame History

Ресёрч: 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
Когда вызывается До и после хендлера Внутри хендлера

Правильная комбинация:

# 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:

# Регистрация в 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):

dp["platform"] = platform_client   # в bot.py

async def handler(message: Message, platform: PlatformClient):  # aiogram сам находит по типу
    ...

Оценка: нужно явно добавить один из этих механизмов, иначе хендлеры не получат platform/store. ⚠️


4. InlineKeyboardBuilder

InlineKeyboardBuilder — рекомендуемый подход в aiogram 3.x. InlineKeyboardMarkup с вложенными списками считается устаревшим стилем.

# 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) — стандарт вместо ручных проверок в хендлерах:

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 для сложных диалогов:

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-фильтры) небольшие и органично вписываются в текущую структуру.