Наверняка после первой части вы думали: «Ну всё, 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:
Домен (Domain) / Предметная область — это та сфера деятельности, для которой вы пишете программу.
Если вы пишете бота для пиццерии, ваш домен — доставка еды.
Если бота-модератора — администрирование чатов. В коде мы должны моделировать именно эти процессы, а не просто перекладывать JSON-файлы.
Единый язык (Ubiquitous Language). Это словарь терминов, который используют и заказчик, и разработчик, и... ваш код.
Плохо: Называть функцию
create_row_in_db, когда вы оформляете заказ.Хорошо (по DDD): Назвать функцию
place_order.Плохо: Называть пользователя
user_obj.Хорошо (по DDD): Если это покупатель —
Customer, если админ —Administrator.
Суть DDD в том, что глядя на код, вы должны читать историю о том, как работает бизнес, а не о том, как работает Python.
А нужно ли это везде?
Тут может возникнуть резонный вопрос: «Не стреляем ли мы из пушки по воробьям? Зачем мне эти сложности в моем пет-проекте?»
Давайте честно: если вы пишете скрипт на один раз, чтобы скачать картинки с сайта, или простейшего бота-эхо — DDD вам не нужен. Это будет избыточно (overengineering).
Однако! Этот подход (или его облегченная версия, как у нас) категорически рекомендуется, если:
Проект будет развиваться. Сегодня у вас 2 команды, завтра 20. Если архитектуры нет, вы утонете в «спагетти-коде».
Логика сложнее, чем «привет-пока». Если есть условия, статусы, переходы, БД — разделение на слои спасает нервы.
Вы учитесь. В пет-проектах мы часто не ограничены дедлайнами, поэтому это лучший полигон, чтобы набить руку на правильных подходах. Лучше научиться писать чисто на кошках, чем страдать потом в продакшене.
Вы работаете не один (или вернетесь к коду через полгода). «Единый язык» помогает быстро понять, что происходит в коде, даже если вы сами его писали, но давно забыли.
Мы используем здесь Pragmatic DDD — берем лучшие практики (слои, язык, сущности), но не закапываемся в академические дебри.
Из чего состоит основа проект?
Проект состоит из слоёв (Layers). Представьте это как слоёный пирог или луковицу. Каждый слой отвечает строго за свою зону ответственности и выполняет конкретные задачи, не лезет в чужие дела.
Обычно выделяют четыре основных слоя (хотя их может быть и больше, и меньше — зависит от задачи):
Domain (Домен / Ядро системы) — самое сердце системы.
Application (Слой приложения) — оркестратор (дирижер) всей системы.
Infrastructure (Инфраструктура) — самый «грязный» слой, где живут все внешние зависимости.
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:Проверит валидность данных.
Создаст сущность
User.Вызовет сохранение в БД.
отправит приветственное письмо.
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 — промежуточное ПО для обработки запросов до того, как они попадут в хэндлер.
Конфигурация приложения
Чтобы практическая часть статьи не превратилась в скучное «создайте десять папок и поверьте мне на слово», мы начнем с фундамента — с конфигурации.
Любой серьезный проект зависит от данных, которые нельзя или неудобно «зашивать» прямо в код (хардкодить).
Эти данные можно разделить на три типа:
Секреты — самая чувствительная информация: токен бота, пароли от базы данных, API-ключи OpenAI. Эти данные никогда не должны попадать в публичный репозиторий (и приватный тоже).
Настройки окружения — параметры, которые меняются в зависимости от того, где запущен бот. Например, на моем ПК (Dev) уровень логирования должен быть
DEBUG, а на сервере (Prod) —INFO.Бизнес-настройки — параметры, которые влияют на логику: 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
Бот запускается.
Dynaconf видит в
.env, чтоENV_FOR_DYNACONF=development.Он берет всё из секции
[default].Сверху «накладывает» настройки из секции
[development].В итоге
logging_levelстановитсяDEBUG.
Это избавляет от кучи условий if debug: ... else: ... прямо в коде.
Переходим к проекту
Теории было много (хоть мы и прошли лишь по верхам), но без неё никуда. А вот практики в этой статье будет чуть меньше — основное написание кода мы оставим на следующие части. Но фундамент заложим прямо сейчас.
Наш план действий:
Создать структуру пакетов:
application,domain,infrastructure,presentation.Настроить конфигурацию: установить
dynaconf, создать файлы настроек и переменные окружения.Написать загрузчик конфига.
Приступим!
Создание пакетов (Python package)
Открываем любимую IDE (PyCharm, VS Code) и создаём в директории src/ai_moderation_bot четыре основных пакета.
Напоминание для новичков: В Python пакетом называется папка, в которой находится файл
init.py. Даже если этот файл пустой, он служит сигналом для интерпретатора Python: «Эй, эта папка — не просто набор файлов, это модуль, из которого можно импортировать код».
Должно получиться так:

Теперь наполним эти слои смыслом, создав вложенные пакеты:
В
domain(Ядро):entities— для сущностей (dataclasses, enums и т.д.).protocols— для интерфейсов/протоколов.
В
application(Приложение):services— здесь будет жить бизнес-логика.
В
infrastructure(Инфраструктура):database— работа с БД (SQLAlchemy).broker— брокер сообщений (Redis).logger— настройки логирования.telegram— обратите внимание: здесь будет лежать клиент бота, мидлвари и фабрики клавиатур (техническая часть).ai_providers— клиенты для нейросетей.
В
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"

того достаточно. Теперь у нас есть:
Место для секретов (
.env).Место для настроек (
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-канал "Код на салфетке" и следите за развитием или даже предлагайте свои идеи!
Благодарю за прочтение, увидимся в следующих статьях!
