Комментарии 33
„Не переусложняйте. Не стоит разрабатывать архитектуры уровня предприятия, когда в действительности нужен маленький и удобный инструмент для настольного компьютера, — это верный рецепт провала.“. После просмотра статьи сложилось впечатление что вы сами своему же совету не следовали. Слишком много воды и переусложнений, по крайней мере на мой вкус.
Согласен, очень похоже на FizzBuzz enterprise Edition. Например как тут https://github.com/blinchk/fizzbuzz-enterprise-edition
Вопрос к @mpanaryin сущность UserUpdate содержит и докстринг описывающий атрибуты класса и сами атрибуты. Может просто описать все в докстринг (в любом формате типа Sphinx, gdoc и. Т. П. ) и класс в декоратор, чтобы создать атрибуты из docstring. Линтерами проверяется точность описания атрибутов.
Я добавлял описание в docstring специально для подсказок IDE при наведении на класс. Когда в коде появляется схема вроде UserUpdate
, без перехода к определению непонятно, какие там поля. А такой docstring позволяет сразу увидеть состав — удобно при чтении и навигации.
Это не альтернатива аннотациям, а просто способ сделать структуру класса наглядной в месте использования.

Но возможно вы имели ввиду что-то другое, если будет возможность, покажите пример
Можно ли сделать всё проще и быстрее? — Конечно.
Но цель была другой: пощупать чистую архитектуру, поработать со всеми слоями. Даже на обычных CRUD'ах это даёт полезный опыт.
Этот проект не говорит «делай так всегда» . Он показывает, как можно сделать, если цель — попрактиковаться в архитектуре.
В тексте статьи подчёркивается зависимость presentation -> infrastructure, но на всех архитектурных схемах указано presentation -> application, что является совершенно логичным. В многослойной архитектуре самый верхний слой presentation, а самый нижний infrastructure. Если в приложении также присутствуют слои application и domain, то прямая зависимость presentation -> infrastructure представляется маловероятной.
"прямая зависимость presentation -> infrastructure представляется маловероятной."
На уровне presentation идёт сборка зависимостей, подобно вот такой:
# presentation/dependencies.py
from typing import Annotated
from fastapi import Depends
from src.users.domain.interfaces.user_uow import IUserUnitOfWork
from src.users.infrastructure.db.unit_of_work import PGUserUnitOfWork
def get_user_uow() -> IUserUnitOfWork:
"""
Dependency that provides an instance of IUserUnitOfWork.
This allows the presentation layer to remain decoupled from the actual implementation.
By default, it returns a PostgreSQL-based unit of work (PGUserUnitOfWork), but the implementation
can be easily overridden for testing or different environments.
:return: IUserUnitOfWork instance.
"""
return PGUserUnitOfWork()
UserUoWDep = Annotated[IUserUnitOfWork, Depends(get_user_uow)]
# presentation/api.py
from src.users.application.use_cases.user_profile import get_user_profile
from src.users.presentation.dependencies import UserUoWDep
@user_api_router.get("/{user_id}", response_model=UserReadDTO)
async def get_profile(user_id: int, uow: UserUoWDep):
"""
Get user profile by ID.
"""
return await get_user_profile(user_id, uow=uow)
Что это, если не прямая зависимость от инфраструктуры "presentation -> infrastructure"?
А почему вы решили, что сборка зависимостей идет на уровне presentation? Не помню такого ни в одной из книг, которые я читал.
верно говорите, сборка зависимостей идет в "main", который знает обо всех частях приложения
Вы правы — если строго по канону, то сборка зависимостей должна происходить в точке входа (main), и именно она "знает всё".
Но в FastAPI часто получается, что зависимости объявляются через Depends(...)
прямо в presentation-слое, а Depends(get_user_uow)
в итоге вызывает конкретную реализацию из infrastructure.
То есть технически это всё ещё прямой импорт, даже если зависимость обёрнута.
Да, можно избежать этого, если прокидывать зависимости снаружи, через контейнер, в main. Но тогда теряется часть удобства, которое FastAPI предоставляет "из коробки".
Очередной компромисс в ущерб строгости.
а
Depends(get_user_uow)
в итоге вызывает конкретную реализацию из infrastructure.
не надо так делать, просто делайте Depends() или с указанием заглушки, а в dependency_overrides пихайте как его создавать. Или можно взять внещний ioc-контейнер, чтобы отделить от возни с парсингом запроса
Спасибо, гляну, а можете назвать конкретные практические плюсы этого подхода? Чем он лучше того что есть сейчас, при условии, что текущий не нарушает Dependency Rule и не создаёт сборную солянку всего в main.py
Текущий нарушает S в слове SRP - отвечает не за представление, а ещё и за связывание компонентов.
Увы, это не практическая, а теоретическая польза.
И при чём тут SPR? Который гласит:
«Модуль должен отвечать за одного и только за одного актора»
Каждый компонент в presentation выполняет строго отведенную ему роль. Это просто самый низкоуровневый слой, может есть для него более пригодное название, я бы с удовольствием посмотрел на варианты.
Если можете, приведите действительно практический пример, который потенциально что-то сломает в этой структуре или что-то усложнит. Потому что совет засунуть в main.py логирование и все зависимости пока кажется сомнительным, ну либо хотя бы покажите свой проект с реализацией, которая кажется вам верной, чтобы можно было на что-то ориентироваться.
Мейн - это не файл и не функция в контексте ЧА. Это отдельный компонент, который занимается настройкой и запуском остального кода.
Если же мы зависимости кладём где-то среди вьюх, у нас появляется две причины редактировать вьюхи: изменение логики представления и изменения компоновки приложения. Логично выделить зависимости отдельно. Окей, теперь у нас вьюхи не содержат кода компоновки, но зависят от него. Если это делать аккуратно, наверно все будет окей, но часто я вижу что такие функции create_db начинают использовать глобальные переменные вроде настроек и сессий. Это стреляет во-первых из-за порядка инициализации (в тестах мы можем хотеть настройки задать после импорта), а во-вторых, из-за связи некоторых таких ресурсов с асинкио лупом. Поэтому если мы и делаем такие зависимости, они ни в коем случае не должны оперировать глобальными переменными. Остальные проявления в виде завязки на реализацию скорее всего не выстрелят, но я предпочитаю вьюхи отделять от остальной инфраструктуры, чтобы они вообще не знали ни о чем кроме интеракторов и может быть каких-то DAO для запросов чтения. Так же, тут есть проблемы что в fastapi при всем преимуществах его механик Depends слабая система управления скоупами, я тут предпочитаю самописный свой контейнер, но это уже совсем другая тема
Если у вас есть возможность, дайте ссылку на свой репозиторий, который бы вы могли назвать "эталонным", было бы интересно поизучать код.
Если же мы зависимости кладём где-то среди вьюх, у нас появляется две причины редактировать вьюхи: изменение логики представления и изменения компоновки приложения.
По своей сути этот слой действительно содержит две ответственности: контроль ввода/вывода и сборка зависимостей. Но они разделены на компоненты. Не будет случая, когда мы меняем что-то одно и обязательно меняется второе.
Если нам нужно будет менять логику представлений, а-ля api.py, то мы соответственно редактируем его.
Например, мы меняем response_model. Опять же никакого влияния на dependencies.Если нужно менять зависимости, то редактируем dependencies.py.
Например, заменив реализацию PGUserUnitOfWork на FakeUserUnitOfWork это никак не отразиться на самом api.py.
Будь у нас реализация, как показано ниже, почти ничего бы не изменилось.
Нужно менять логику представления?— идём в api.py
Нужно менять зависимости? — идём в соответствующий файл, только теперь это main
Но, конечно, не могу не согласиться, что теперь api.py не будет напрямую зависеть (иметь импорт) от компоновки зависимостей.
# main
def get_user_uow() -> IUserUnitOfWork:
return PGUserUnitOfWork()
app.dependency_overrides[IUserUnitOfWork] = get_user_uow
# presentation/api.py
@user_api_router.get("/{user_id}", response_model=UserReadDTO)
async def get_profile(user_id: int, uow: IUserUnitOfWork = Depends()):
"""
Get user profile by ID.
"""
return await get_user_profile(user_id, uow=uow)
async
def
get(self, url: str, **kwargs): ...
это не интерфейс, это невнятный огрызок. Интерфейс нужен чтобы зафиксировать требования к реализации, а у вас тут kwargs торчит.
core
– это базовый модуль приложения. Здесь определяется конфигурация системы, базовая модель данных, базовые типы ошибок, константы, переиспользуемые клиенты (redis, elasticsearch), настройки логгеров.
Так базовые вещи или детали реализации, определитесь. Базовая модель данных - звучит как кусок "домена"/слоя "entites" в то время как клиент redis - кусок конкретной части инфраструктуры, а настройки логгерыов это вообще мейн
crud
– это вспомогательный модуль. Он реализует генератор CRUD-операций и FastAPI роутеров для них.
CRUD операции существуют на всех слоях приложения, с точки зрения архитектуры бессмысленный набор слов. Хотите выделить слой - выделите слой по зоне ответсвенности/уровню абстракции, а не по наличию там 4 типичных операций. CRUD может быть в DAO/Репозитории, может быть в HTTP клиете, могут быть интеракторы обслуживающие этот круд, а могут быть хэндлеры слоя представления. Если у вас нет бизнес логики - не пытайтесь выдавить из себя её, сделайте один слой presentation (назовите api, views или ещё как) и расслабьтесь. Как только появится что-то сложнее одного вызова шлюза БД - выделяйте уже сервисный слой по всем правилам.
utils
– это ещё один вспомогательный модуль, который не должен зависеть от других. Содержит утилиты, которые могут использоваться в любом месте проекта, и именно поэтому модуль должен оставаться как можно более независимым.
это какие такие утилиты? почти в каждом проекте где такой пакет есть - это свлака кусков с совершенно разной зонов ответственности. Первое время такое делать точно не надо, выделяйте общие куски внутри слоев и называйте их нормально, utils сам по себе родится из хаоса, но старайтесь как можно дольше обходиться без этого
Этот "невнятный огрызок" и есть удобная сигнатура для реализации.
Хотите строгий контракт — пожалуйста, но потеряете гибкость.from aiohttp.client import _RequestOptions @classmethod async def post(cls, url: str, **kwargs: _RequestOptions) -> aiohttp.ClientResponse: """ Execute HTTP POST request. Args: url (str): HTTP POST request endpoint. **kwargs (_RequestOptions): Defaults kwargs for aiohttp request Returns: response: HTTP POST request response - aiohttp.ClientResponse object instance. """ client = cls.get_aiohttp_client() response = await client.post(url, **kwargs) return response
То как вы структурируете - ваше дело, тут описан текущий проект.
"Базовые" не в плане "высокоуровневые", а в плане общие, то от чего можно отталкиваться.
Вcore
есть 2 папки: domain и infrastructure. Не сложно будет понять, что это переиспользуемые функции для всего проекта. В entity определена базовая pydantic схема, в infrastructure - то, что свойственно ей: клиенты, настроки... Они не меняются от модуля к модулю и их где-то нужно хранить.crud
- компромисс, и он задокументирован. Это FastAPI-утилита, а не архитектурный слой. В основном проекте не используется. Просто утиль, который может быть полезен, если кому-то лень писать однотипные ручки руками.utils
- просто переиспользуемый функционал, который сложно отнести к конкретному модулю. Что в нём плохого?
Этот "невнятный огрызок" и есть удобная сигнатура для реализации.
Самая неудобная, получается что КАЖДАЯ реализация должна поддерживать ПРОИЗВОЛЬНО число аргументов. А тот кто вызывает всё равно не знает что туда пихать. Вы экономите 6 минут на написании кода и подкладывается большую говняшку тому, кто будет это поддерживать
"Базовые" не в плане "высокоуровневые", а в плане общие, то от чего можно отталкиваться.
ну так настройки логгинга - это не общая вещь, это максимальная частная вещь, которая относится к кода запуска. Вы буквально перечислили несколько вещей совершенно разных по смыслу и уровню абстракции, но все запихнули в core. Почему у вас domain то в core, то отдельно я не понял снова.
crud
- компромисс, и он задокументирован. Это FastAPI-утилита, а не архитектурный слой.
Если бы это была фастапи утилита, она бы лежала где-то внутри слоя presentation и реализовывала только детали работы с fastapi. Какая зона ответсвенности у неё в вашем приложении совершенно непонятно, я вижу там и работу с БД и генерацию вьюх. Мой совет - сначала выстраивайте архитектуру приложения, а потом оптимизируйте конкретные куски, сейчас это выглядит как попытка срезать углы и нарушение всех принципов описанных дальше.
просто переиспользуемый функционал, который сложно отнести к конкретному модулю. Что в нём плохого?
в том, что такой функциональности исчезающе мало и ей можно дать нормальное название.
КАЖДАЯ реализация должна поддерживать ПРОИЗВОЛЬНО число аргументов
Да, потому что интерфейс заточен под работу с готовыми HTTP-клиентами, а не конкретную ручку. В реальности — мне нужен только URL. Всё остальное — дело конкретной реализации (aiohttp, httpx и т.п.).
ну так настройки логгинга - это не общая вещь, это максимальная частная вещь, которая относится к кода запуска
Почему нет? Это как раз общая настройка: определяю один раз и использую в main.py
. Лучше так, чем размазывать частные конфигурации по слоям.
curd
: сейчас это выглядит как попытка срезать углы и нарушение всех принципов описанных дальше.
Я об этом сам прямо написал в статье. Это утиль, не часть архитектуры. В основном проекте он не используется. Можно не обращать внимания.
в том, что такой функциональности исчезающе мало и ей можно дать нормальное название.
Можете предложить свой вариант. И я не говорю, что её много. Просто она может существовать.
Вы буквально перечислили несколько вещей совершенно разных по смыслу и уровню абстракции, но все запихнули в core. Почему у вас domain то в core, то отдельно я не понял снова.
Потому что core
— не "ядро бизнес-логики", а место для общих вещей. Клиенты Redis/ES, настройки, константы — это shared-инфраструктура. Подмодули domain
и infrastructure
нужны для понимания, куда это потом ляжет.
Я делю и по смыслу (users
, vacancies
, core
), и по слоям (domain
, application
, infrastructure
, presentation
). core
— это техническая база, не предметная область. Если название смутило — возможно, стоит выбрать другое.
Всё остальное — дело конкретной реализации (aiohttp, httpx и т.п.).
почему у вас в интерфейсе появились детали реализации? интерфейс нужен для абстрагирования
Это как раз общая настройка: определяю один раз и использую в
main.py
. Лучше так, чем размазывать частные конфигурации по слоям.
ну вот сами сказали - один раз настроили и используется в мейн. Это часть мейна, а никакая не "общая часть".
Это утиль, не часть архитектуры
архитектура описывает вообще всё приложение, утили вписываются в него. ваша утиль пересекает несколько архитектурных слоев. Это может быть ок, но тогда мы должны аккуратненько её отложить в сторону и выстроить вокруг неё заборчик. Условно - сделать модульный монолит.
Можете предложить свой вариант. И я не говорю, что её много. Просто она может существовать.
мой вариант: в каждом конкретном случае решать куда положить тот или иной кусок кода и назвать модуль согласно тому что там и за что оно отвечает, а не utils.
почему у вас в интерфейсе появились детали реализации? интерфейс нужен для абстрагирования
Я всё понимаю, возможно он излишне упрощен и недоработан. Возможно, из-за того, что я знал как он будет использоваться.
Он просто определяет контракт для сервиса, который будет делать все запросы в едином формате.
class APIClientService(AuthMixin):
def __init__(
self,
client: IAsyncHttpClient,
source_url: str,
headers: dict | None = None,
auth_type: AuthType = AuthType.NO,
username: str | None = None,
password: str | None = None,
token: str | None = None
):
self.auth_type = auth_type
self.username = username
self.password = password
self.token = token
self.client = client
self.source_url = source_url
self.headers = {**(headers or {}), **self.auth_headers}
async def request(
self,
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"],
endpoint: str,
json_data: dict | None = None,
params: dict | None = None,
headers: dict | None = None,
**kwargs
):
headers = headers or {}
request_params = {
"url": urljoin(self.source_url, endpoint),
"headers": {**self.headers, **headers},
"json": json_data, "params": params, **kwargs
}
if method == "GET":
response = await self.client.get(**request_params)
elif method == "POST":
response = await self.client.post(**request_params)
elif method == "PUT":
response = await self.client.put(**request_params)
elif method == "DELETE":
response = await self.client.delete(**request_params)
elif method == "PATCH":
response = await self.client.patch(**request_params)
else:
raise ValueError("Method not supported")
response.raise_for_status()
return response
ну вот сами сказали - один раз настроили и используется в мейн. Это часть мейна, а никакая не "общая часть".
Ну видите, это как рассудить. Вы говорите, что это часть мейна, т.е. вы хотите в main.py засунуть весь LOGGING_CONFIG и всё что ему нужно для работы? Handlers, Filters...? Нужно ли это всё держать в main.py? Я считаю, что нет. Поэтому он и вынесен.
А говорю, что "общая часть" потому что по сути этот конфиг логирования будет применен для всех логов в проекте.
архитектура описывает вообще всё приложение, утили вписываются в него. ваша утиль пересекает несколько архитектурных слоев
Какой-то бессмысленный разговор. Я говорю, что crud
можно просто выкинуть из текущего проекта и ничего не изменится. Он ему не нужен. Я его оставил лишь по той причине, что кому-то может пригодится код для быстрого прототипирования роутеров. Я не настаиваю на том, что он соблюдает принципы чистой архитектуры или подходит под этот проект. Это всё было сказано в самой статье.
мой вариант: в каждом конкретном случае решать куда положить тот или иной кусок кода и назвать модуль согласно тому что там и за что оно отвечает, а не utils.
Можете дать ссылку на репозиторий какого-нибудь своего проекта, взглянуть о чем речь, как именно вы структурируете всё?
Прикладные (функциональные)
Эти модули реализуют ключевую бизнес-логику приложения. Они строго структурированы по слоям: domain, application, infrastructure, presentation.
Нет. Это не ключевая бизнес логика. Вы перечислили слои из которых состоит ВСЁ приложение. То что вы переричслили выше раскиыдвается точно так же по этим самым слоям. Слоев может быть больше или меньше, но у вас явные пересечения ответсвенности. Бизнес логика здесь потценциально в application и domain. К слову, в ЧА нет слоя domain, там есть entites и это деление отличается от DDD. В DDD прикладной слой фактически не содержит бизнес логики, это обслуживающая штука, в то время как в ЧА у нас бизнес логика разделена на две части - более универсальную (entities) и относяющууся к конкретным сценариям (application).
Вы придираетесь к словам "реализуют ключевую бизнес-логику приложения", предполагая, что там на каждом уровне есть бизнес логика? Конечно, нет. Они для этого и разделены на domain, application, infrastructure, presentation.
Это противопоставление описанным общим-техническим, в которых бизнес-логики нет вообще.
ваш слой CRUD это буквально невнятно вырезанный presentation, но я если честно так и не понял что он делает
Если речь до сих пор про crud
, то ещё ещё раз повторюсь - это не отдельный архитектурный слой. По сути это вообще рудимент и не относится к этой статье, в которой несколько раз упоминалось о его побочных эффектах.
В нём описан функционал для быстрого прототипирования CRUD операций и создания под них роутеров в FastAPI.
Пример его применения был в статье:
class VacancyService(CRUDBase, model=orm.VacancyDB):
"""
Infrastructure-level service for low-level CRUD operations on VacancyDB.
"""
class VacancyCRUDRouter(CRUDRouter):
crud = VacancyService()
create_schema = VacancyCreateDTO
update_schema = VacancyUpdateDTO
read_schema = VacancyReadDTO
router = APIRouter()
По итогу сгенерируется следующее:

Вот в эту статью буду тыкать тех, кто будет заикаться про чистую архитектуру... ~двадцать сущностей только чтоб логин-пароль ввести.. :)
Всё просто - эта статья противоречит чистой архитектуре. Просто настаскали сомниельных подходов из всего интернета, забив на все основные концепции
Есть ли в этом проекте с его текущим функционалом переусложнение - несомненно. Но на чём-то же нужно было показывать пример.
Потому что логин — это не просто "ввёл и прошёл". Это:
валидация
поиск пользователя
проверка пароль
работа с токенами
учёт ролей
ограничение доступа
и так далее
Можно сделать проще? Конечно, можно. Хоть в одном фале напиши. Используй готовое, забей на слои. Всё будет работать.
Короче, как я понимаю, при формальном подходе к чистой архитектуре (где всё разложено по слоям и полочкам) написание кода должно выглядеть примерно так (предположим, нам надо завести пользователя и поприветствовать его):
# file_1
class IUser:
@abstractmethod
def check_email(self):
...
# file_2
class User(IUser):
def __init__(self, name, email):
self.name = name
self.email = email
def check_email(self):
...
# file_3
@dataclass
class UserDTO:
name: str
email: str
# file_4
def greeting(user_dto):
print(f'Привет {user_dto.name}')
# file_5
user = User('vasia', 'vasia@pupkin.co')
user_dto = UserDTO()
user_dto.name = user.name
user_dto.email = user.email
greeting(user_dto)
Вместо ужасного грязного кода:
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def greeting(self):
print(f'Привет {self.name}')
def check_email(self):
...
user = User('vasia', 'vasia@pupkin.co')
user.greeting()
:)
Забавная карикатура :) Оптимально? Конечно, нет.
Писать "чистую архитектуру" сложно и не всегда оправдано. Да и мнений, как именно "правильно", — десятки. Даже в этом простом проекте хватает своих "но", и он не следует строго канону дяди Боба — скорее своя сборная солянка.
Что на счёт такого подхода к User. В маленьких проектах всё обычно просто. Но когда код начинает расти, User может стать god-object’ом: валидирует, шлёт письма, пишет в базу, логинит… Наверное, проблемы появятся, просто не сразу и когда появятся, то возможно будет больно их править.
«Чистая архитектура» это сферический конь в вакууме.
Как и чистый код.
Единственное для чего по-моему годится чистый код- это тыкать неродивых девелоперов носом и говорить «пиши как велели в Чистом коде». При этом на любой вопрос почему? Можно спокойно отвечать потому… видя какой треш народ пишет, и не понимает когда тонко насекают что можно/нужно по-другому (хотя бы не писать 100 вложенных if)
Если я верно уловил мысль, то "чистый код" - это нечто идеалистическое. Концепции, которые трудно применить в реальной практике без перегибов и усложнений.
Скорее всего это так и есть (по крайней мере я согласен), ну либо как минимум у программиста должен быть огромный опыт, чтобы делать это всё почти интуитивно.
Когда Дядя Боб написал статью "Читая архитектура" у него уже было 40+ лет коммерческого опыта. Он приводил в примеры проекты, которые теряли миллионы долларов на том, что неверно спроектировали систему. И конечно, эти системы не являются веб-приложением из трёх функций.
Поэтому как и в заключении этой статьи, моя мысль в том, что не стоит переусложнять в реальных проектах. Но иметь широкий кругозор полезно, чтобы можно было аргументировать почему именно так было сделано, а не потому что ты другого способа не знаешь.
Чистая архитектура - это в первую очередь книжка про SOLID, доугие принципы и чуточку про слои. При чем там говорится что слои надо делать исходя из потребностей проекта. Так что формальное следование ей в целом очень сомнительно, только если читать часть, игнорируя остальное.
Разбираем архитектуру. Часть 2. Чистая архитектура на примере FastAPI приложения