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