2022-03-23 17:54:43
FastAPI и Dependency Injection.
В таком популярном фреймворке как FastAPI есть механизм для автоматизации управления зависимостями в рамках концепции DI.
Однако в документации автор местами путается, плюс есть некоторые особенности данного фреймворка, влияющие на удобство использования.
Что такое
Dependency Injection?
Это достаточно простая концепция, которая говорит: если объекту что-то нужно, он не должен знать, как оно создается.
Пример:
Ваш класс User хочет уметь ходить в Shop.
Он мог бы создать магазин сам, но тогда мы не сможем всем пользователям дать один магазин. И вообще придется переносить в него всю логику строительства магазинов со всеми вариантами её применения.
Вместо этого предлагается чтобы User получал экземпляр Shop в __init__.
И уже наш код, создающий пользователей, сможет передать туда нужный магазин. При чем если у нас пользователи бывают в разных местах системы мы сможем в каждом случае иметь правильные магазины и свою логику работы с ними.
Таким образом весь механизм DI состоит из двух частей:
1. Есть класс/функция, которая от чего-то зависит
2. Есть логика, которая подставляет эту зависимость
Замечание:
DI - хорошая вещь, но не увлекайтесь слишком сильным дроблением кода. Не стоит применять эту концепцию для частей, являющихся деталью реализации самого класса.
В случае FastAPI, у нас есть механизм автоматической подстановки зависимостей в наши view-функции (это обычно называется IoC-контейнер).
Он состоит аналогично из двух частей
* С помощью Depends мы обозначаем зависимость. Если зависимость идентифицируется по аннотации типа параметра функции, то Depends используется без параметров.
* С помощью app.dependency_overrides мы определяем фабрику, возвращающую эту зависимость или генераторную функцию
В простом случае это может выглядеть вот так:
# определяем view
@router.get
def my_view_funcion(param: Session = Depends()):
...
# в мейне создаем приложение и настраиваем
app = FastApi()
app.dependency_overrides[Session] = some_session_factory
Типичной ошибкой, которую допускает даже автор fastapi является указать настоящую функцию создания зависимостей в Depends
# так делать не стоит
def my_view_funcion(param: Session = Depends(create_session)): ...
В этом случае вы конечно все ещё можете переопределить зависимость в тестах, но ваш код оказывается сцеплен с кодом создания объекта Session и более того, вы не можете настроить логику работы функции create_session при создании приложения.
То есть это не DI, а только его половинка.
Функционально Depends в fastapi можно использовать не только для DI, но и для переиспользования части логики или даже параметров запроса и это накладывает свой отпечаток.
Дело в том, что FastAPI генерирует open api (swagger) спецификацию для view-функции не только на основе её параметров, но так же на основе параметров её зависимостей.
Поэтому, если наша зависимость (класс Session) имеет конструктор с параметрами, мы увидим их в swagger-спецификации и для отключения этого нет встроенных средств.
Избежать это можно двумя способами:
1. Указывая зависимость на абстрактный базовый, а не на конкретный класс. Это можно быть хорошей идеей с точки зрения структурирования кода, но бывает сложно описать, когда мы зависим от сторонних классов
2. Указывая зависимость на
функцию-заглушку, которая ничего на самом деле не делает. В том числе это можно применить для маркировки однотипных зависимостей, но которые должны иметь разные значения.
Первый способ в коде выглядит так
class MyProto(abc.ABC):
...
@router.get
def my_view_funcion(param: MyProto = Depends()):
...
app = FastApi()
app.dependency_overrides[MyProto] = some_session_factory
Второй способ - так:
def get_session_stub():
raise NotImplementedError # это реально тело этой функции
@router.get
def my_view_funcion(param: Session = Depends(get_session_stub)):
...
app = FastApi()
app.dependency_overrides[get_session_stub] = some_session_factory
3.2K viewsedited 14:54