Dependency Injection (DI) — это концепция в программировании, которая позволяет управлять зависимостями между компонентами кода. Обычно это достигается за счёт внедрения необходимых ресурсов в функции или классы извне, что делает их более гибкими и легко тестируемыми. В традиционных программах, когда один объект создаёт или инициализирует другой, между ними создается жесткая связь. С использованием DI зависимости предоставляются извне, что упрощает взаимодействие между компонентами.
Примеры применения DI и его преимущества
Dependency Injection можно представить в виде услуги, которая поставляется извне. Например, класс, отвечающий за регистрацию пользователей, может нуждаться в доступе к базе данных для сохранения информации о новых пользователях и к сервису email для отправки приветственного письма. При использовании DI мы передаем объекты для работы с базой данных и отправки email в класс регистрации, не создавая их внутри. Это позволяет легко заменить или протестировать каждый компонент без изменения самого класса регистрации.
Преимущества DI:
- Модульность: Код становится более независимым, так как компоненты легко заменяются.
- Тестируемость: DI упрощает тестирование кода, поскольку позволяет подменять реальные зависимости на фиктивные (mock) или тестовые.
- Гибкость: Упрощается добавление и изменение логики, например, мы можем легко заменить службу отправки email на другую, просто внедрив новый компонент.
Теперь, когда у нас есть базовое понимание DI, давайте рассмотрим, как эта концепция реализуется в FastAPI.
Dependency Injection в FastAPI
FastAPI — это фреймворк для создания веб-API, который предоставляет встроенную поддержку Dependency Injection. Одной из особенностей DI в FastAPI является автоматическая передача зависимостей с помощью функции Depends
, которая упрощает инъекцию зависимостей. Dependency Injection в FastAPI поддерживает автоматическое внедрение объектов в функции, которые требуют внешних ресурсов, таких как:
- Подключение к базе данных
- Настройки конфигурации
- Аутентификационные токены
- Внешние API
FastAPI распознает зависимости, объявленные через Depends
, и автоматически их создает и передает в нужные функции. Таким образом, DI помогает избежать дублирования кода и позволяет нам сосредоточиться на основной логике.
Пример базовой зависимости в FastAPI
Чтобы лучше понять работу DI в FastAPI, начнем с простого примера зависимости. Допустим, у нас есть функция, которая возвращает текущее время, и мы хотим использовать это время в различных эндпоинтах нашего API.
from fastapi import FastAPI, Depends
from datetime import datetime
app = FastAPI()
# Зависимость, возвращающая текущее время
def get_current_time():
return datetime.now()
@app.get("/time")
def read_time(current_time: datetime = Depends(get_current_time)):
return {"current_time": current_time}
В этом примере:
- Функция
get_current_time
возвращает текущее время. - Эндпоинт
/time
используетDepends
, чтобы получить значение времени изget_current_time
.
Каждый раз, когда мы вызываем /time
, FastAPI автоматически вызывает get_current_time
, передает результат в read_time
и возвращает значение пользователю.
Dependency Injection с базой данных в FastAPI
Теперь создадим более сложный пример с подключением к базе данных. Предположим, что мы хотим реализовать API, где пользователи могут запрашивать информацию из базы данных. Мы используем SQLAlchemy
для управления базой данных и создаем отдельную функцию-зависимость для подключения к ней.
Шаг 1: Настройка подключения к базе данных
Сначала создаем базу данных и функцию для подключения:
# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Функция-зависимость для создания и закрытия сессии базы данных
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
Функция get_db
создает сессию базы данных, а затем yield
передает ее в нужную функцию, при этом сессия автоматически закрывается после завершения запроса.
Шаг 2: Использование зависимости в эндпоинте
Теперь добавим эндпоинт, который будет использовать эту зависимость для выполнения запроса к базе данных. Предположим, у нас есть таблица пользователей, и мы хотим получить пользователя по его идентификатору:
# main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from . import database # импортируем модуль с функцией get_db
app = FastAPI()
# Модель пользователя
class User:
id: int
name: str
# Функция для получения пользователя из БД
def get_user(db: Session, user_id: int):
user = db.query(User).filter(User.id == user_id).first()
return user
# Эндпоинт с использованием зависимости get_db
@app.get("/users/{user_id}")
def read_user(user_id: int, db: Session = Depends(database.get_db)):
user = get_user(db, user_id=user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
Здесь db: Session = Depends(get_db)
указывает, что функция get_db
будет вызвана автоматически, а возвращаемое значение будет передано в db
. Это освобождает нас от необходимости вручную создавать и закрывать сессию для каждого запроса, упрощая взаимодействие с базой данных.
Продвинутые примеры Dependency Injection
Помимо базы данных, Dependency Injection также полезен для таких компонентов, как конфигурационные настройки, авторизация и работа с внешними API.
1. Dependency Injection для аутентификации
Для веб-API часто требуется проверка подлинности пользователя. В следующем примере создадим зависимость, которая проверяет наличие токена и возвращает пользователя.
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
token = credentials.credentials
# Логика для проверки и декодирования токена
if token != "valid_token": # простая проверка для примера
raise HTTPException(status_code=401, detail="Invalid or expired token")
return {"username": "test_user"}
Теперь добавим зависимость в эндпоинт:
@app.get("/protected")
def protected_route(user: dict = Depends(get_current_user)):
return {"message": f"Hello, {user['username']}!"}
2. Dependency Injection для внешних API
Зависимости также могут быть полезны для обработки запросов к внешним сервисам. Например, если API интегрируется с внешним сервисом погоды, DI упростит обновление или замену этого сервиса.
import requests
def get_weather_data(city: str):
response = requests.get(f"http://api.weather.com/v3/weather/{city}")
return response.json()
Эту функцию можно использовать в любом эндпоинте:
@app.get("/weather/{city}")
def weather(city: str, weather_data=Depends(lambda: get_weather_data(city))):
return weather_data
Тестирование и настройка DI для тестов
Одним из преимуществ DI является легкость замены зависимостей на тестовые объекты. В FastAPI тестирование можно проводить с помощью TestClient
и фиктивных зависимостей.
Пример теста для зависимости базы данных:
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
# Подменим get_db на фиктивное подключение
def override_get_db():
# Создаем тестовую базу данных или используем фиктивные данные
yield mock_db_session # фиктивная сессия
app.dependency_overrides[get_db] = override_get_db
# Тестирование эндпоинта
def test_read_user():
response = client.get("/users/1")
assert response.status_code == 200
Заключение
Dependency Injection в FastAPI позволяет строить API, которые легко расширять и поддерживать. Использование DI улучшает тестируемость и модульность кода, позволяя передавать зависимости динамически. В FastAPI DI реализован с помощью Depends
, что делает фреймворк удобным и гибким для создания любых веб-приложений, будь то простые API или сложные микросервисные архитектуры.