Слоистая архитектура, также известная как Onion Architecture, является подходом к построению программного обеспечения, который обеспечивает высокую степень изолированности и легкость сопровождения. В данной статье мы рассмотрим, как применить слоистую архитектуру на FastAPI, популярном фреймворке для создания веб-приложений на Python.
Слоистая архитектура состоит из нескольких уровней, каждый из которых отвечает за определенные аспекты приложения. Эти уровни обычно разделены на слои представления (Presentation), бизнес-логики (Domain), доступа к данным (Data Access) и внешних зависимостей (External Dependencies). FastAPI является отличным инструментом для реализации данной архитектуры благодаря своей поддержке зависимостей и быстрому созданию API.
Шаг 1: Создание структуры проекта
Первым шагом при применении слоистой архитектуры на FastAPI является создание структуры проекта. Мы можем разделить наш проект на следующие каталоги:
- app: здесь будут находиться модули, отвечающие за представление, бизнес-логику и доступ к данным.
- core: здесь будут находиться общие компоненты, такие как модели данных, ошибки и константы.
После создания структуры проекта мы можем начать создавать слои нашего приложения.
Шаг 2: Создание слоя представления
Слой представления отвечает за обработку запросов от клиентов и передачу данных в слой бизнес-логики. В FastAPI мы можем создать view-функции, которые будут обрабатывать HTTP-запросы и вызывать методы нашего сервиса.
Пример view-функции для создания нового пользователя:
from fastapi import APIRouter
from app.services.user_service import create_user
router = APIRouter()
@router.post("/users", response_model=User)
async def create_user_handler(user_data: UserCreate):
user = await create_user(user_data)
return user
Шаг 3: Создание слоя бизнес-логики
Слой бизнес-логики содержит логику нашего приложения, которая отвечает за обработку данных и принятие решений. В FastAPI мы можем создать сервисы, которые будут выполнять бизнес-логику.
Пример сервиса для создания нового пользователя:
from app.models.user import User, UserCreate
from app.repositories.user_repository import UserRepository
async def create_user(user_data: UserCreate) -> User:
user_repo = UserRepository()
user = User(**user_data.dict())
return await user_repo.create(user)
Шаг 4: Создание слоя доступа к данным
Слой доступа к данным отвечает за взаимодействие с базой данных или другими источниками данных. В FastAPI мы можем использовать ORM или любую другую технологию доступа к данным.
Пример репозитория для доступа к данным пользователя:
from app.models.user import User
class UserRepository:
async def create(self, user: User) -> User:
# Сохранение пользователя в базу данных
return user
Шаг 5: Создание слоя внешних зависимостей
Слой внешних зависимостей отвечает за взаимодействие с внешними сервисами или библиотеками. В FastAPI мы можем использовать зависимости для инъекции зависимостей в наши сервисы.
Пример инъекции зависимости в сервис создания пользователя:
from app.services.user_service import create_user
from app.external_dependencies.email_service import send_email
async def create_user_handler(user_data: UserCreate):
user = await create_user(user_data)
await send_email(user.email, "Welcome to our platform!")
Итак, мы рассмотрели основные шаги по созданию слоистой архитектуры на FastAPI. При использовании данного подхода мы можем легко масштабировать и изменять наше приложение, а также повысить его стабильность и понятность кода. Надеемся, что данная статья была полезной и поможет вам создать качественное веб-приложение на FastAPI.
💡 Попробуй онлайн-тренажёр для подготовки к техническому собеседованию: https://clck.ru/3B5gwP 💡
Забирай роадмап изучения самого востребованного фреймворка на Python – FastAPI здесь: https://t.me/ArtemShumeikoBot
1. Переопределяешь сигнатуры дочерних классов, что не ок.
2. Не показал, как работаешь с селектом и перегоняешь модели между слоями
Какой-то Java EE. Нет никаких проблем писать sql в эндпоинтах. Он как раз гораздо понятнее и нагляднее там сидит. В крайнем случае для длинных запросов можно вынести выражение в отдельный модуль.
Когда у вас с фронта просят ручки со вложенными сущностями вам нужно делать joinedload/selectinload. Где-то вам нужен User+Tasks, где-то только User. Вы на каждый такой вариант будете делать по функции в репозитории? Или передавать опции отдельным параметром, ещё больше всё усложняя?
Модели и схемы можно очень быстро посмотреть одним кликом из эндпоинта, а вот прыгать по ORM надстройкам кастомным – это как раз разбираться нужно.
Ну и по поводу притянутой за уши задачи уровня "смены sqlalchemy" – такое не происходит почти никогда. Да и на что? Вы скорее на другой язык переписывать будете.
а это нормально что мы каждый раз будем писать with async_session_maker() ? в каждом методе
Спасибо за видео! Репозитории же по сути и есть DAO, или я что то путаю?
Очень удобно получилось.
Никто не поймет, пока не объяснишь.
Люблю такие архитектуры
22:51 указана неправильная типизация входного параметра tasks_repo, должно быть Type[AbstractRepository]. Тогда и подсказки для self.tasks_repo корректно будут работать без дублирования типа
Кажется, не хватает dipendecy injection
А что с join делать? В смысле мы в методах получаем просто id для FK, а хорошо бы полную модельку получить с подтянутыми полями. Если в наследнике репозитория переопределить метод и добавить join, то смысл базового репозитория теряется
А какой смысл каждый раз при вызове TaskService передавать в него TaskRepository? Разве не логичнее в конструкторе TaskService объявить что-нибудь а-ля self.repo = TaskRepository()?
Честно говоря, за более чем 4 года коммерческой разработки ни разу не сталкивался с тем, чтобы нужно было менять реляционную базу на NoSQL или в обратку, если такая потребность вдруг возникнет, то это свидетельствует об архитектурном просчёте просвете, и что уж там греха таить, изменение ручек – меньшее из зол.
Так называемая луковая структура с первого взгляда может показаться удобной, но как мне видится, создаёт дополнительную и не очень нужную среднюю абстракцию сервисов. Несколько проектов плотного использования стека из видео привели меня (да, и как мне видится, всю команду) к тому, что между роутами и крудами – абстракция, которая похожа на репозиторий, но ориентированная на sql и в ней никогда не происходит коммитов (лично бью по рукам на ревью), либо ничего нет и идёт вызов методов базовых методов, либо стоят какие-то усложнены методы, которые использую базовые, в простейших случаях из примера там может быть реализована большая часть бизнес логики. Эти методы проксируют инстанс сессии через себя, агрегируют в общем случае множество базовых запросов но не коммитят результат (в подавляющем большинстве) , при этом, очевидно, что они все в одной транзакции, а это база безопасности при работе с бд, ибо действия которые приводят к некосистентности базы – то, с чем нужно бороться на корню и без апелляций.
Спасибо в целом за в видео, для себя подчеркнул, что пора уже начинать пользоваться анатомиями роутах, а то совсем как дед – вижу, но продолжаю отрицать, поддерживая никому не нужную совместимость с более старыми версиями fastapi. С Новым Годом, процветание каналу.
Спасибо за видео! Что ты лумаешь по поводу того, чтобы использовать Протоколы вместо абстрактных базовых классов? Протоколы позволяют задать имена и типы атрибутов класса/экземпляра, параметров методов, а также типы возвращаемых методами значений. Это не получится сделать с помощью ABC.
Неплохо, но не до конца. Нужно добавить дженерик в объявление абстрактного репозитория и возвращать его в методах.
а чем она отличается от чистой архитектура Дяди Боба
Каким образом лучше организовать код, если нужно написать специфичный sql запрос, который нужен только для определенного сервиса? Писать функцию внутри сервиса, либо создавать новый репозиторий?
Фабрика еще нужна что бы выбирать конкретную реализацию репозитория
весьма интересно, однозначно лайк!
не канал, а находка 👍
Привет! Хмм а когда ты делаешь в find_all методе res = [row[0].to_read_model это не нужно никак обложить уровнем абстракции так же?
Т.е такой кейс, ты меняешь алхимию на что то другое, откуда ты знаешь что у тебя модельки соответствуют колу to_read_model внутри реализации SQLNotAlchemyRepository?
promo sm