Layered Architecture with FastAPI / Onion Architecture

Posted by


Слоистая архитектура, также известная как 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.

0 0 votes
Article Rating
41 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
@artemshumeiko
1 month ago

💡 Попробуй онлайн-тренажёр для подготовки к техническому собеседованию: https://clck.ru/3B5gwP 💡

Забирай роадмап изучения самого востребованного фреймворка на Python – FastAPI здесь: https://t.me/ArtemShumeikoBot

@linust5892
1 month ago

1. Переопределяешь сигнатуры дочерних классов, что не ок.
2. Не показал, как работаешь с селектом и перегоняешь модели между слоями

@teawithoutdonuts31
1 month ago

Какой-то Java EE. Нет никаких проблем писать sql в эндпоинтах. Он как раз гораздо понятнее и нагляднее там сидит. В крайнем случае для длинных запросов можно вынести выражение в отдельный модуль.

Когда у вас с фронта просят ручки со вложенными сущностями вам нужно делать joinedload/selectinload. Где-то вам нужен User+Tasks, где-то только User. Вы на каждый такой вариант будете делать по функции в репозитории? Или передавать опции отдельным параметром, ещё больше всё усложняя?

Модели и схемы можно очень быстро посмотреть одним кликом из эндпоинта, а вот прыгать по ORM надстройкам кастомным – это как раз разбираться нужно.

Ну и по поводу притянутой за уши задачи уровня "смены sqlalchemy" – такое не происходит почти никогда. Да и на что? Вы скорее на другой язык переписывать будете.

@xesax
1 month ago

а это нормально что мы каждый раз будем писать with async_session_maker() ? в каждом методе

@best_coozy_dad
1 month ago

Спасибо за видео! Репозитории же по сути и есть DAO, или я что то путаю?

@onikun2120
1 month ago

Очень удобно получилось.
Никто не поймет, пока не объяснишь.
Люблю такие архитектуры

@Olegt0rr
1 month ago

22:51 указана неправильная типизация входного параметра tasks_repo, должно быть Type[AbstractRepository]. Тогда и подсказки для self.tasks_repo корректно будут работать без дублирования типа

@VaeV1ct1s
1 month ago

Кажется, не хватает dipendecy injection

@ЕвгенийБондарев-э1ц
1 month ago

А что с join делать? В смысле мы в методах получаем просто id для FK, а хорошо бы полную модельку получить с подтянутыми полями. Если в наследнике репозитория переопределить метод и добавить join, то смысл базового репозитория теряется

@superpadush
1 month ago

А какой смысл каждый раз при вызове TaskService передавать в него TaskRepository? Разве не логичнее в конструкторе TaskService объявить что-нибудь а-ля self.repo = TaskRepository()?

@BadekYO
1 month ago

Честно говоря, за более чем 4 года коммерческой разработки ни разу не сталкивался с тем, чтобы нужно было менять реляционную базу на NoSQL или в обратку, если такая потребность вдруг возникнет, то это свидетельствует об архитектурном просчёте просвете, и что уж там греха таить, изменение ручек – меньшее из зол.
Так называемая луковая структура с первого взгляда может показаться удобной, но как мне видится, создаёт дополнительную и не очень нужную среднюю абстракцию сервисов. Несколько проектов плотного использования стека из видео привели меня (да, и как мне видится, всю команду) к тому, что между роутами и крудами – абстракция, которая похожа на репозиторий, но ориентированная на sql и в ней никогда не происходит коммитов (лично бью по рукам на ревью), либо ничего нет и идёт вызов методов базовых методов, либо стоят какие-то усложнены методы, которые использую базовые, в простейших случаях из примера там может быть реализована большая часть бизнес логики. Эти методы проксируют инстанс сессии через себя, агрегируют в общем случае множество базовых запросов но не коммитят результат (в подавляющем большинстве) , при этом, очевидно, что они все в одной транзакции, а это база безопасности при работе с бд, ибо действия которые приводят к некосистентности базы – то, с чем нужно бороться на корню и без апелляций.
Спасибо в целом за в видео, для себя подчеркнул, что пора уже начинать пользоваться анатомиями роутах, а то совсем как дед – вижу, но продолжаю отрицать, поддерживая никому не нужную совместимость с более старыми версиями fastapi. С Новым Годом, процветание каналу.

@green1278dramost5
1 month ago

Спасибо за видео! Что ты лумаешь по поводу того, чтобы использовать Протоколы вместо абстрактных базовых классов? Протоколы позволяют задать имена и типы атрибутов класса/экземпляра, параметров методов, а также типы возвращаемых методами значений. Это не получится сделать с помощью ABC.

@anton6643
1 month ago

Неплохо, но не до конца. Нужно добавить дженерик в объявление абстрактного репозитория и возвращать его в методах.

@JIJI-zv1qp
1 month ago

а чем она отличается от чистой архитектура Дяди Боба

@MS-sf3pk
1 month ago

Каким образом лучше организовать код, если нужно написать специфичный sql запрос, который нужен только для определенного сервиса? Писать функцию внутри сервиса, либо создавать новый репозиторий?

@sergeysergey421
1 month ago

Фабрика еще нужна что бы выбирать конкретную реализацию репозитория

@begula_chan
1 month ago

весьма интересно, однозначно лайк!

@TechnoBog-ov2mp
1 month ago

не канал, а находка 👍

@TheFany666
1 month ago

Привет! Хмм а когда ты делаешь в find_all методе res = [row[0].to_read_model это не нужно никак обложить уровнем абстракции так же?

Т.е такой кейс, ты меняешь алхимию на что то другое, откуда ты знаешь что у тебя модельки соответствуют колу to_read_model внутри реализации SQLNotAlchemyRepository?

@chelseamurphy6799
1 month ago

promo sm