Наверняка после первой части вы думали: «Ну всё, uv настроили, сейчас быстренько накидаем хэндлеров в main.py и запустим».

Не тут-то было! Мы пойдем по «взрослому» пути и начнем сразу с хардкора — с архитектуры проекта. Почему? Потому что хороший дом начинается не с поклейки обоев, а с надежного фундамента и подробного чертежа. Если мы пропустим этот этап сейчас, через месяц разработки наш проект превратится в запутанный клубок кода, который страшно трогать.

В этой статье мы:

  • Разберём теорию: Поймем, что такое DDD (Domain-Driven Design) и зачем делить проект на слои.

  • Построим структуру: Создадим правильные директории и пакеты, чтобы всё лежало на своих местах.

  • Настроим конфиг: Подключим мощную библиотеку dynaconf, чтобы забыть о хардкоде токенов и паролей.

Кстати, я веду Telegram-канал Код на салфетке, где публикую заметки и гайды для новичков. Буду рад видеть вас там!

Важно: эта статья — часть серии «ИИ бот-модератор». Она опирается на код и архитектурные решения, описанные в предыдущих материалах. Если вы попали сюда напрямую, некоторые места могут показаться неполными или слишком короткими — это нормально, просто в рамках цикла мы практически не повторяем уже разобранное.


Что такое архитектура?

Архитектура — это одновременно и подробный чертёж проекта, и его фундамент. Если проект, созданный нами с помощью uv, можно сравнить со строительной площадкой, то архитектура — это то, на чём всё здание будет держаться.

От выбора архитектурного подхода напрямую зависит не только то, как будет вестись разработка сейчас, но и то, насколько больно (или приятно) будет поддерживать и развивать проект через полгода.

Я не зря назвал архитектуру чертежом. Она определяет «географию» проекта: где лежат файлы, за что они отвечают и как общаются друг с другом. Это критически важный аспект разделения ответственности (Separation of Concerns).

Хорошая архитектура избавляет от мучительных поисков и вопроса: «А где там у меня эта функция лежит?». Ты открываешь проект и сразу понимаешь: «Ага, логика тут, работа с базой здесь, а кнопки вот там». Это позволяет быстро ориентироваться даже в проектах с десятками и сотнями файлов.

Какую архитектуру выберем мы?

Как я упоминал в первой части, мы будем знакомиться с подходами DDD (Domain-Driven Design).

Сделаю важную оговорку: мы не будем внедрять «академический» DDD во всей его строгости. Это сложная и глубокая тема, которая для небольших проектов может стать избыточной (overengineering). В теоретическом блоке мы пройдёмся по верхам, чтобы понять суть, а на практике реализуем «прагматичную» версию, адаптированную под разработку Telegram-бота.

Что такое DDD?

Domain-Driven Design (Предметно-ориентированное проектирование) — это подход к разработке, при котором в центре внимания находится не база данных и не красивые кнопочки, а Предметная область (Domain) и её логика.

Проще говоря, код должен максимально точно отражать реальные бизнес-процессы и использовать те же термины, которые используются в реальной жизни.

О чем речь?

Чтобы стало понятнее, разберем два ключевых понятия DDD:

  1. Домен (Domain) / Предметная область — это та сфера деятельности, для которой вы пишете программу.

    • Если вы пишете бота для пиццерии, ваш домен — доставка еды.

    • Если бота-модератора — администрирование чатов. В коде мы должны моделировать именно эти процессы, а не просто перекладывать JSON-файлы.

  2. Единый язык (Ubiquitous Language). Это словарь терминов, который используют и заказчик, и разработчик, и... ваш код.

    • Плохо: Называть функцию create_row_in_db, когда вы оформляете заказ.

    • Хорошо (по DDD): Назвать функцию place_order.

    • Плохо: Называть пользователя user_obj.

    • Хорошо (по DDD): Если это покупатель — Customer, если админ — Administrator.

Суть DDD в том, что глядя на код, вы должны читать историю о том, как работает бизнес, а не о том, как работает Python.

А нужно ли это везде?

Тут может возникнуть резонный вопрос: «Не стреляем ли мы из пушки по воробьям? Зачем мне эти сложности в моем пет-проекте?»

Давайте честно: если вы пишете скрипт на один раз, чтобы скачать картинки с сайта, или простейшего бота-эхо — DDD вам не нужен. Это будет избыточно (overengineering).

Однако! Этот подход (или его облегченная версия, как у нас) категорически рекомендуется, если:

  1. Проект будет развиваться. Сегодня у вас 2 команды, завтра 20. Если архитектуры нет, вы утонете в «спагетти-коде».

  2. Логика сложнее, чем «привет-пока». Если есть условия, статусы, переходы, БД — разделение на слои спасает нервы.

  3. Вы учитесь. В пет-проектах мы часто не ограничены дедлайнами, поэтому это лучший полигон, чтобы набить руку на правильных подходах. Лучше научиться писать чисто на кошках, чем страдать потом в продакшене.

  4. Вы работаете не один (или вернетесь к коду через полго��а). «Единый язык» помогает быстро понять, что происходит в коде, даже если вы сами его писали, но давно забыли.

Мы используем здесь Pragmatic DDD — берем лучшие практики (слои, язык, сущности), но не закапываемся в академические дебри.

Из чего состоит основа проект?

Проект состоит из слоёв (Layers). Представьте это как слоёный пирог или луковицу. Каждый слой отвечает строго за свою зону ответственности и выполняет конкретные задачи, не лезет в чужие дела.

Обычно выделяют четыре основных слоя (хотя их может быть и больше, и меньше — зависит от задачи):

  1. Domain (Домен / Ядро системы) — самое сердце системы.

  2. Application (Слой приложения) — оркестратор (дирижер) всей системы.

  3. Infrastructure (Инфраструктура) — самый «грязный» слой, где живут все внешние зависимости.

  4. Presentation (Слой представления) — точка входа в систему со стороны пользователя.

Взаимодействие идет строго в одном направлении: снаружи — внутрь. Внешние слои знают о внутренних, но внутренние ничего не знают о внешних.

[ Presentation ] -> знает про -> [ Application ] -> знает про -> [ Domain ]
                                       ^
[ Infrastructure ] --------------------|

Инфраструктура здесь стоит особняком: она зависит от Домена, но используется Приложением через абстракции. Сложно? Сейчас разберем!

Тем, кто привык писать ботов в одном файле main.py или просто делить папки по типу handlers, keyboards, utils, такая структура поначалу кажется избыточной. Но поверьте, как только ваш бот вырастет из 300 строк в 3000, вы скажете этому разделению «спасибо». Вы всегда будете знать: «Логика кнопок — тут, работа с базой — там, а бизнес-правила — вот здесь».

Разберём каждый слой подробнее.

1. Слой Домена (Domain)

Здесь живут правила бизнеса, которые вообще не меняются (или меняются крайне редко).

Главная фишка этого слоя: он ничего не знает о внешнем мире. Домен не знает, что у нас база данных PostgreSQL или SQLite, не знает, что мы используем aiogram, не знает про HTTP-запросы. Он существует в вакууме. Он описывает чистую логику.

Основные директории:

  • Entities (Сущности) — здесь лежат наши дата-классы (dataclasses), описывающие объекты реального мира (User, Chat, Subscription), а также вспомогательные типы (Enum, TypedDict).

  • Protocols (Протоколы/Интерфейсы) — на языке Java/C# это называется интерфейсами. Это «контракты» или «должностные инструкции».

    • Пример: Мы пишем протокол UserSaver, у которого есть метод save_user. Домену всё равно, как и куда мы сохраним пользователя (в файл, в БД или отправим голубиной почтой). Главное — описать контракт. Это и есть Python-реализация принципа Dependency Inversion через утиную типизацию.

2. Слой Приложения (Application)

Это «менеджер» или «дирижер» проекта. Сам он ничего сложного не считает (это делает Домен) и ничего в базу не пишет (это делает Инфраструктура). Его задача — взять данные, пнуть нужные сервисы и вернуть результат.

В нём располагаются:

  • Services (Сервисы) — классы, в которых описана последовательность действий.

    • Пример: Сервис RegistrationService. Его метод register:

      1. Проверит валидность данных.

      2. Создаст сущность User.

      3. Вызовет сохранение в БД.

      4. отправит приветственное письмо.

  • Use Cases (Сценарии использования) — иногда выделяют отдельные классы под каждое действие (один класс — один метод execute). Но в нашем проекте, чтобы не плодить тысячи файлов, мы ограничимся Сервисами, группирующими схожие сценарии.

3. Слой Инфраструктуры (Infrastructure)

Здесь находится всё, от чего технически зависит проект: работа с конкретной БД, внешние API, Redis, логирование, файловая система. Это «чернорабочий» слой.

Структура обычно выглядит так:

  • database — настройки подключения к БД, модели ORM (SQLAlchemy), миграции.

  • http_clients — клиенты для запросов к внешним API (например, aiohttp).

  • logging — настройка логгера.

  • providers — провайдеры для внедрения зависимостей (мы будем использовать Dishka).

Здесь применяются важные паттерны:

  • Repository (Репозиторий) — класс, который реализует Протокол из Домена. Именно здесь мы пишем SQL-запросы (select, insert). Для Домена это просто абстрактное «хранилище», а здесь — конкретный код SQLAlchemy.

  • Adapter (Адаптер) — обертка над чужим кодом, чтобы он подходил под наши Протоколы.

4. Слой Презентации (Presentation)

Точка входа. Его задача — принять «сырой» сигнал от пользователя, быстро проверить, что пришло нечто адекватное, передать управление в Слой Приложения и, получив ответ, красиво показать его пользователю.

В мире aiogram это:

  • Routers & Handlers — наши функции с декораторами @router.message().

  • Schemas / DTO — схемы (обычно на Pydantic) для валидации входящих данных.

  • Middlewares — промежуточное ПО для обработки запросов до того, как они попадут в хэндлер.


Конфигурация приложения

Чтобы практическая часть статьи не превратилась в скучное «создайте десять папок и поверьте мне на слово», мы начнем с фундамента — с конфигурации.

Любой серьезный проект зависит от данных, которые нельзя или неудобно «зашивать» прямо в код (хардкодить).

Эти данные можно разделить на три типа:

  1. Секреты — самая чувствительная информация: токен бота, пароли от базы данных, API-ключи OpenAI. Эти данные никогда не должны попадать в публичный репозиторий (и приватный тоже).

  2. Настройки окружения — параметры, которые меняются в зависимости от того, где запущен бот. Например, на моем ПК (Dev) уровень логирования должен быть DEBUG, а на сервере (Prod) — INFO.

  3. Бизнес-настройки — параметры, которые влияют на логику: ID администраторов, часовые пояса, тексты приветствий.

Для управления всем этим зоопарком мы будем использовать библиотеку Dynaconf.

Почему Dynaconf?

Обычно новички используют просто файл .env и библиотеку python-dotenv. Это хороший старт, но Dynaconf — это следующий уровень. Это «швейцарский нож» для конфигураций.

Его главные фишки:

  • Всеядность: Он умеет собирать настройки отовсюду: из переменных окружения, .env файлов, TOML, YAML, JSON и объединять их в один удобный объект settings.

  • Слои окружений: Вы можете один раз прописать настройки для тестов, разработки и продакшена, и переключаться между ними одной переменной. Не нужно переписывать файлы вручную!

Разберем два кита, на которых будет стоять наш конфиг.

Переменные окружения и .env

Переменные окружения (Environment Variables) — это значения, которые живут в операционной системе, а не в коде Python. Это стандарт индустрии для передачи секретов.

Чтобы не задавать их каждый раз в консоли вручную, их прописывают в файле .env в корне проекта.

Важно: Файл .env должен быть добавлен в .gitignore! Он содержит ваши секреты. В репозиторий обычно кладут файл .env.example с пустыми значениями, чтобы другие разработчики знали, что заполнять.

Синтаксис файла предельно прост: КЛЮЧ=ЗНАЧЕНИЕ.

Пример .env-файла:

# Указываем текущее окружение (development/production)
ENV_FOR_DYNACONF=development

# Секретные данные
BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
ADMIN_ID=123456789
OPENAI_API_KEY=sk-proj-....

# Подключение к БД (обычно тоже секрет)
DATABASE_URL=postgresql://user:pass@localhost:5432/mybot_db
  • Ключ: Принято писать в ВЕРХНЕМ_РЕГИСТРЕ. По этому ключу мы будем искать значение в коде.

  • Значение: Пишется после =. Кавычки нужны только если в значении есть пробелы или спецсимволы.

#### Файл настроек (settings.toml)

Для параметров, которые не являются секретными, но важны для логики бота, Dynaconf позволяет использовать удобные форматы вроде TOML или YAML. Мы выберем TOML (он сейчас стандарт в мире Python, вспомните pyproject.toml).

Этот файл (например, settings.toml или config/settings.toml) можно и нужно хранить в Git.

В чем "магия" Dynaconf? Он позволяет создавать вложенные секции для разных окружений.

Пример settings.toml:

[default]
# Эти настройки применяются всегда, если их не переопределили
app_name = "ModeratorBot"
version = "1.0.0"
timezone = "Europe/Moscow"
parse_mode = "HTML"
logging_level = "INFO"

[development]
# Эти настройки применятся ТОЛЬКО если в .env стоит ENV_FOR_DYNACONF=development
# Они перезапишут значения из [default]
logging_level = "DEBUG"
debug_payments = true  # Включаем тестовые платежи

[production]
# А это для боевого сервера
logging_level = "WARNING"
debug_payments = false
  1. Бот запускается.

  2. Dynaconf видит в .env, что ENV_FOR_DYNACONF=development.

  3. Он берет всё из секции [default].

  4. Сверху «накладывает» настройки из секции [development].

  5. В итоге logging_level становится DEBUG.

Это избавляет от кучи условий if debug: ... else: ... прямо в коде.


Переходим к проекту

Теории было много (хоть мы и прошли лишь по верхам), но без неё никуда. А вот практики в этой статье будет чуть меньше — основное написание кода мы оставим на следующие части. Но фундамент заложим прямо сейчас.

Наш план действий:

  1. Создать структуру пакетов: application, domain, infrastructure, presentation.

  2. Настроить конфигурацию: установить dynaconf, создать файлы настроек и переменные окружения.

  3. Написать загрузчик конфига.

Приступим!

Создание пакетов (Python package)

Открываем любимую IDE (PyCharm, VS Code) и создаём в директории src/ai_moderation_bot четыре основных пакета.

Напоминание для новичков: В Python пакетом называется папка, в которой находится файл init.py. Даже если этот файл пустой, он служит сигналом для интерпретатора Python: «Эй, эта папка — не просто набор файлов, это модуль, из которого можно импортировать код».

Должно получиться так:

Теперь наполним эти слои смыслом, создав вложенные пакеты:

  1. В domain (Ядро):

    • entities — для сущностей (dataclasses, enums и т.д.).

    • protocols — для интерфейсов/протоколов.

  2. В application (Приложение):

    • services — здесь будет жить бизнес-логика.

  3. В infrastructure (Инфраструктура):

    • database — работа с БД (SQLAlchemy).

    • broker — брокер сообщений (Redis).

    • logger — настройки логирования.

    • telegramобратите внимание: здесь будет лежать клиент бота, мидлвари и фабрики клавиатур (техническая часть).

    • ai_providers — клиенты для нейросетей.

  4. В presentation (Представление):

    • handlers — а вот здесь будут лежать сами обработчики команд (то, что отвечает пользователю).

В итоге структура должна выглядеть примерно так:

Отлично. Скелет проекта собран. Теперь нужно научить этот скелет читать настройки. Переходим к конфигурации.

Файлы конфигурации

Теперь создадим файлы, в которых будут жить настройки нашего бота. Мы пропишем начальные параметры — впоследствии они могут измениться или расшириться, но для старта этого хватит.

Файл .env (Секреты)

В корне проекта создадим два файла: .env и .env.example.

  • .env — здесь хранятся реальные секреты (токены, пароли). Этот файл мы обязательно добавляем в .gitignore, чтобы он не улетел в интернет.

  • .env.example — это шаблон. В нём мы перечисляем те же ключи, но с пустыми или фиктивными значениями. Этот файл мы отправляем в репозиторий. Так любой разработчик (или вы сами через полгода) поймет: «Ага, для запуска проекта мне нужно создать .env и заполнить вот эти поля».

Заполним .env следующими переменными (обратите внимание на синтаксис: знак равно без пробелов):

# --- База данных (PostgreSQL) ---
POSTGRES_USER=moderator_user
POSTGRES_PASSWORD=strong_password
POSTGRES_DB=moderator_db
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432

# URL для подключения (используется SQLAlchemy)
# Формат: postgresql+asyncpg://USER:PASSWORD@HOST:PORT/DB_NAME
POSTGRES_URL=postgresql+asyncpg://moderator_user:strong_password@127.0.0.1:5432/moderator_db

# --- Бот и Telegram ---
BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11

# --- Redis (для FSM и кэша) ---
REDIS_URL=redis://127.0.0.1:6379/0

Совет: Пока что можете заполнить эти значения "заглушками" (например, BOT_TOKEN=change_me).

Файл settings.toml (Настройки)

Также в корне проекта создадим файл settings.toml. Здесь будут лежать настройки логики, которые не являются секретными.

[default]
# Основные настройки
ADMIN_ID = 123456
ERRORS_CHAT_ID = 1234567

# Настройки бота
PARSE_MODE = "HTML"
TIMEZONE = "Europe/Moscow"

того достаточно. Теперь у нас есть:

  1. Место для секретов (.env).

  2. Место для настроек (settings.toml).

Осталось написать код, который объединит эти два файла в удобный Python-объект.

Установка dynaconf и класс конфигурации

Остался последний рывок перед тем, как мы сможем запустить бота (ну, почти). Нам нужно "подружить" наши файлы настроек с Python-кодом.

Установка библиотеки

Если вы используете uv, как мы договаривались, установка выполняется одной командой в терминале:

uv add dynaconf

Теперь Dynaconf доступен в нашем проекте.

Интеграция конфигурации

В пакете src/ai_moderation_bot (рядом с файлом init.py) создадим файл config.py.

Почему здесь? Конфигурация — это глобальная вещь, она нужна и слою базы данных, и слою хэндлеров. Поэтому мы кладем её в корень пакета бота, чтобы её было удобно импортировать отовсюду.

Как обычно делают (но мы сделаем лучше):

Обычно инициализация выглядит так:

settings = Dynaconf(
    settings_files=["settings.toml"],
    # ... параметры
)

Это работает, но есть проблема: ваша IDE (VS Code или PyCharm) не знает, какие настройки лежат внутри переменной settings. Вы пишете settings., а редактор молчит. Никаких подсказок, никаких типов. Приходится помнить названия переменных наизусть.

Как сделаем мы:

Мы создадим класс Settings, унаследованный от Dynaconf. Внутри мы явно пропишем поля и их типы. Это даст нам шикарное автодополнение и проверку типов по всему проекту.

Открываем src/ai_moderation_bot/config.py и пишем:

from dynaconf import Dynaconf


class Settings(Dynaconf):
    # Явно указываем типы полей для IDE
    ADMIN_ID: int
    ERRORS_CHAT_ID: int
    PARSE_MODE: str
    TIMEZONE: str

    # Секретные данные из .env
    POSTGRES_URL: str
    BOT_TOKEN: str
    REDIS_URL: str


# Инициализация настроек
settings = Settings(
    # Список файлов, где искать настройки (можно указывать несколько)
    settings_files=["settings.toml"],
    # Отключаем префикс для переменных окружения (по умолчанию DYNACONF_)
    envvar_prefix=False,
    # Включаем поддержку слоев [development], [production] и т.д.
    environments=True,
    # Автоматически загружать переменные из файла .env
    load_dotenv=True,
)

Разберем аргументы Settings(...):

  • settings_files=["settings.toml"] — говорим библиотеке: "Возьми настройки по умолчанию из этого файла".

  • environments=True — активирует механизм слоев. Dynaconf посмотрит в переменную окружения ENV_FOR_DYNACONF (которую мы задали в .env) и, если там написано development, подгрузит соответствующую секцию из toml-файла.

  • load_dotenv=True — принуд��тельно заставляет библиотеку прочитать файл .env и загрузить секреты.

  • envvar_prefix=False — позволяет читать переменные как есть (BOT_TOKEN), а не требовать префикса (DYNACONF_BOT_TOKEN).

Полный код файла:

from dynaconf import Dynaconf  


class Settings(Dynaconf):  
    ADMIN_ID: int  
    ERRORS_CHAT_ID: int  
    PARSE_MODE: str  
    TIMEZONE: str  

    POSTGRES_URL: str  
    BOT_TOKEN: str  
    REDIS_URL: str  


settings = Settings(  
    settings_files=["settings.toml"],  
    envvar_prefix=False,  
    environments=True,  
    load_dotenv=True,  
)

На этом основная настройка завершена.

Мы подготовили мощный фундамент: разбили проект на слои по DDD и организовали удобную конфигурацию.


Заключение

В этой статье снова было не так много кода, зато мы разобрали критически важную теорию. Да, мы прошлись по ней «по верхам», но я считаю, что это отличная точка входа. Настоящее понимание архитектуры приходит не сразу, а только через практику — методом проб, ошибок, рефакторинга и снова ошибок.

Дальше будет много интересного и, возможно, непростого, но точно полезного! В следующих частях мы займемся базой данных и внедрением зависимостей. Кода будет становиться всё больше, а «сухой» теории — всё меньше.

Подписывайтесь на Telegram-канал "Код на салфетке" и следите за развитием или даже предлагайте свои идеи!

Благодарю за прочтение, увидимся в следующих статьях!