Максим Панарьин @mpanaryin
Пользователь
Информация
- В рейтинге
- Не участвует
- Откуда
- Москва, Москва и Московская обл., Россия
- Зарегистрирован
- Активность
Специализация
Бэкенд разработчик, Веб-разработчик
Средний
Python
Django
FastAPI
PostgreSQL
Nginx
Redis
Docker
Clean Architecture
Английский язык
Нашёл время просмотреть проект (без запуска, в общих чертах). Вот мои мысли:
Что понравилось:
Структурированный
main
Выделены роуты, админка, middleware и конфигурируемый запуск приложения.
Все настройки (
config/settings
) собраны в одном месте:Я тоже об этом думал. В своём проекте выносил настройки, связанные только с авторизацией, внутрь модуля
auth
, думая, зачем им быть в общей "свалке", если они касаются только одной части системы. Но с другой стороны, чтобы быть последовательным, пришлось бы все настройки раскидать по модулям, и тогда структура стала бы гораздо более фрагментированной и потенциально хаотичной. Так что подход «всё в одном месте» в итоге выглядит предпочтительнее.Celery вынесен и сконфигурирован сразу как production-ready
Бизнес-логика в
use-case
, а не в репозитории (на примере обновления пользователя):Логика проверки существования пользователя реализована именно на уровне
use-case
, а не репозитория. Скорее всего это имеет больше смысла, иначеuse-case
превращается просто в тонкую обёртку над репозиторием. К тому же в реальном проекте именно на этом уровне будет сосредоточена основная бизнес-логика, и хранить часть в репозитории, а часть в use-case будет странно.Отдельное спасибо за тесты. Я ранее ими не особо увлекался, поэтому увидеть их в правильном виде особенно полезно.
Что можно обсудить:
Именование файлов глаголами (по аналогии с функциями):
Такое решение видно в
use-case
иapi-router
. Обычно я так не делаю, не знаю насколько это допустимо.Использование UUID как ID в сущности
User
:Благодаря UUID можно генерировать
id
прямо вuse-case
, что позволяет создаватьUser
до обращения к базе. Но если бы у нас была необходимость использоватьinteger
какid
, возникли бы сложности: пришлось бы делатьid: int | None = None
, потому чтоuse-case
не может сам сгенерировать валидныйint
-ID (он обычно автоинкрементный). Это делаетUser
-сущность с необязательным ID немного странной семантически — она начинает походить наvalue object
. Наверное, именно по этой причине я в своём проекте передавал в репозиторий специализированные сущности а-ля UserCreate.Импорт
use-case
через__init__.py
на примереauthenticate
:Пока
use-case
один, это выглядит аккуратно. Но как только их станет несколько — придётся явно их выносить. Держать всё в__init__.py
в долгосрочной перспективе вряд ли удобно.Как итог: спасибо, было интересно поглядеть на альтернативу, я подметил для себя пару интересных вещей. Если будут конкретные вопросы или темы, которые хотели бы обсудить тут или в личке, то всегда рад.
Я, наверное, не стану в десятый раз писать, что каждый сам выберет, как ему больше нравится, и что текущий проект не "руководство" к тому, как нужно делать, а лишь пример, как можно делать. Кому-то может пригодится что-то из этого проекта, кому-то нет.
И если этот проект кому-то покажется over-кодом, то я тут пару дней назад наткнулся на один проектик, уж что эти люди скажут про него и представить страшно...
Спасибо за комментарий. Думаю, вопрос о "правильности" чистой архитектуры — это вечная тема для обсуждений и у каждого есть своё мнение) Кто-то может по 5-10 лет работать через Active Record и не видеть никакой проблемы.
Я лишь стараюсь подчеркнуть важную мысль: архитектура — это прежде всего осознанный выбор. Если решение принято, его нужно уметь обосновать: почему именно так, а не потому что "по-другому не умею".
Что касается использования
pydantic
вdomain
в моём случае это сознательное допущение: мне просто спокойнее, когда данные валидируются на любом уровне, и я могу удобно с ними работать, не повторяя лишнюю логику. Это компромисс между строгостью и прагматизмом, с которым мне комфортно жить в рамках текущего проекта.Но описанные вами проблемы действительно рано или поздно могут настигнуть, если это будет большой, долгоживущий проект. Так что тут опять просто отталкиваемся от наших целей.
На счёт репозитория - да, конечно, будет интересно глянуть
Если у вас есть возможность, дайте ссылку на свой репозиторий, который бы вы могли назвать "эталонным", было бы интересно поизучать код.
По своей сути этот слой действительно содержит две ответственности: контроль ввода/вывода и сборка зависимостей. Но они разделены на компоненты. Не будет случая, когда мы меняем что-то одно и обязательно меняется второе.
Если нам нужно будет менять логику представлений, а-ля api.py, то мы соответственно редактируем его.
Например, мы меняем response_model. Опять же никакого влияния на dependencies.
Если нужно менять зависимости, то редактируем dependencies.py.
Например, заменив реализацию PGUserUnitOfWork на FakeUserUnitOfWork это никак не отразиться на самом api.py.
Будь у нас реализация, как показано ниже, почти ничего бы не изменилось.
Нужно менять логику представления?— идём в api.py
Нужно менять зависимости? — идём в соответствующий файл, только теперь это main
Но, конечно, не могу не согласиться, что теперь api.py не будет напрямую зависеть (иметь импорт) от компоновки зависимостей.
Если я верно уловил мысль, то "чистый код" - это нечто идеалистическое. Концепции, которые трудно применить в реальной практике без перегибов и усложнений.
Скорее всего это так и есть (по крайней мере я согласен), ну либо как минимум у программиста должен быть огромный опыт, чтобы делать это всё почти интуитивно.
Когда Дядя Боб написал статью "Читая архитектура" у него уже было 40+ лет коммерческого опыта. Он приводил в примеры проекты, которые теряли миллионы долларов на том, что неверно спроектировали систему. И конечно, эти системы не являются веб-приложением из трёх функций.
Поэтому как и в заключении этой статьи, моя мысль в том, что не стоит переусложнять в реальных проектах. Но иметь широкий кругозор полезно, чтобы можно было аргументировать почему именно так было сделано, а не потому что ты другого способа не знаешь.
Увы, это не практическая, а теоретическая польза.
И при чём тут SPR? Который гласит:
Каждый компонент в presentation выполняет строго отведенную ему роль. Это просто самый низкоуровневый слой, может есть для него более пригодное название, я бы с удовольствием посмотрел на варианты.
Если можете, приведите действительно практический пример, который потенциально что-то сломает в этой структуре или что-то усложнит. Потому что совет засунуть в main.py логирование и все зависимости пока кажется сомнительным, ну либо хотя бы покажите свой проект с реализацией, которая кажется вам верной, чтобы можно было на что-то ориентироваться.
Спасибо, гляну, а можете назвать конкретные практические плюсы этого подхода? Чем он лучше того что есть сейчас, при условии, что текущий не нарушает Dependency Rule и не создаёт сборную солянку всего в main.py
Я всё понимаю, возможно он излишне упрощен и недоработан. Возможно, из-за того, что я знал как он будет использоваться.
Он просто определяет контракт для сервиса, который будет делать все запросы в едином формате.
Ну видите, это как рассудить. Вы говорите, что это часть мейна, т.е. вы хотите в main.py засунуть весь LOGGING_CONFIG и всё что ему нужно для работы? Handlers, Filters...? Нужно ли это всё держать в main.py? Я считаю, что нет. Поэтому он и вынесен.
А говорю, что "общая часть" потому что по сути этот конфиг логирования будет применен для всех логов в проекте.
Какой-то бессмысленный разговор. Я говорю, что
crud
можно просто выкинуть из текущего проекта и ничего не изменится. Он ему не нужен. Я его оставил лишь по той причине, что кому-то может пригодится код для быстрого прототипирования роутеров. Я не настаиваю на том, что он соблюдает принципы чистой архитектуры или подходит под этот проект. Это всё было сказано в самой статье.Можете дать ссылку на репозиторий какого-нибудь своего проекта, взглянуть о чем речь, как именно вы структурируете всё?
Вы правы — если строго по канону, то сборка зависимостей должна происходить в точке входа (main), и именно она "знает всё".
Но в FastAPI часто получается, что зависимости объявляются через
Depends(...)
прямо в presentation-слое, аDepends(get_user_uow)
в итоге вызывает конкретную реализацию из infrastructure.То есть технически это всё ещё прямой импорт, даже если зависимость обёрнута.
Да, можно избежать этого, если прокидывать зависимости снаружи, через контейнер, в main. Но тогда теряется часть удобства, которое FastAPI предоставляет "из коробки".
Очередной компромисс в ущерб строгости.
Забавная карикатура :) Оптимально? Конечно, нет.
Писать "чистую архитектуру" сложно и не всегда оправдано. Да и мнений, как именно "правильно", — десятки. Даже в этом простом проекте хватает своих "но", и он не следует строго канону дяди Боба — скорее своя сборная солянка.
Что на счёт такого подхода к User. В маленьких проектах всё обычно просто. Но когда код начинает расти, User может стать god-object’ом: валидирует, шлёт письма, пишет в базу, логинит… Наверное, проблемы появятся, просто не сразу и когда появятся, то возможно будет больно их править.
Да, потому что интерфейс заточен под работу с готовыми HTTP-клиентами, а не конкретную ручку. В реальности — мне нужен только URL. Всё остальное — дело конкретной реализации (aiohttp, httpx и т.п.).
Почему нет? Это как раз общая настройка: определяю один раз и использую в
main.py
. Лучше так, чем размазывать частные конфигурации по слоям.Я об этом сам прямо написал в статье. Это утиль, не часть архитектуры. В основном проекте он не используется. Можно не обращать внимания.
Можете предложить свой вариант. И я не говорю, что её много. Просто она может существовать.
Потому что
core
— не "ядро бизнес-логики", а место для общих вещей. Клиенты Redis/ES, настройки, константы — это shared-инфраструктура. Подмодулиdomain
иinfrastructure
нужны для понимания, куда это потом ляжет.Я делю и по смыслу (
users
,vacancies
,core
), и по слоям (domain
,application
,infrastructure
,presentation
).core
— это техническая база, не предметная область. Если название смутило — возможно, стоит выбрать другое.Если речь до сих пор про
crud
, то ещё ещё раз повторюсь - это не отдельный архитектурный слой. По сути это вообще рудимент и не относится к этой статье, в которой несколько раз упоминалось о его побочных эффектах.В нём описан функционал для быстрого прототипирования CRUD операций и создания под них роутеров в FastAPI.
Пример его применения был в статье:
По итогу сгенерируется следующее:
Есть ли в этом проекте с его текущим функционалом переусложнение - несомненно. Но на чём-то же нужно было показывать пример.
Потому что логин — это не просто "ввёл и прошёл". Это:
валидация
поиск пользователя
проверка пароль
работа с токенами
учёт ролей
ограничение доступа
и так далее
Можно сделать проще? Конечно, можно. Хоть в одном фале напиши. Используй готовое, забей на слои. Всё будет работать.
Вы придираетесь к словам "реализуют ключевую бизнес-логику приложения", предполагая, что там на каждом уровне есть бизнес логика? Конечно, нет. Они для этого и разделены на domain, application, infrastructure, presentation.
Это противопоставление описанным общим-техническим, в которых бизнес-логики нет вообще.
Этот "невнятный огрызок" и есть удобная сигнатура для реализации.
Хотите строгий контракт — пожалуйста, но потеряете гибкость.
То как вы структурируете - ваше дело, тут описан текущий проект.
"Базовые" не в плане "высокоуровневые", а в плане общие, то от чего можно отталкиваться.
В
core
есть 2 папки: domain и infrastructure. Не сложно будет понять, что это переиспользуемые функции для всего проекта. В entity определена базовая pydantic схема, в infrastructure - то, что свойственно ей: клиенты, настроки... Они не меняются от модуля к модулю и их где-то нужно хранить.crud
- компромисс, и он задокументирован. Это FastAPI-утилита, а не архитектурный слой. В основном проекте не используется. Просто утиль, который может быть полезен, если кому-то лень писать однотипные ручки руками.utils
- просто переиспользуемый функционал, который сложно отнести к конкретному модулю. Что в нём плохого?"прямая зависимость presentation -> infrastructure представляется маловероятной."
На уровне presentation идёт сборка зависимостей, подобно вот такой:
Что это, если не прямая зависимость от инфраструктуры "presentation -> infrastructure"?
Я добавлял описание в docstring специально для подсказок IDE при наведении на класс. Когда в коде появляется схема вроде
UserUpdate
, без перехода к определению непонятно, какие там поля. А такой docstring позволяет сразу увидеть состав — удобно при чтении и навигации.Это не альтернатива аннотациям, а просто способ сделать структуру класса наглядной в месте использования.
Но возможно вы имели ввиду что-то другое, если будет возможность, покажите пример
Можно ли сделать всё проще и быстрее? — Конечно.
Но цель была другой: пощупать чистую архитектуру, поработать со всеми слоями. Даже на обычных CRUD'ах это даёт полезный опыт.
Этот проект не говорит «делай так всегда» . Он показывает, как можно сделать, если цель — попрактиковаться в архитектуре.
Спасибо) готовлю статью, сам проект уже написан, останется подрефакторить перед публикацией
Благодарю!