Перейти к основному содержимому

Мини-курс

мы пройдём через некоторые из основных концепций, связанных с веб-разработкой, которые помогут вам лучше понять наши руководства.

Resource (Ресурс)​

Любая идентифицируемая сущность, к которой можно получить доступ через URL​

Не усложняйте—если вам не нравится термин resource (ресурс), думайте о нём как об объекте.

Entity (Сущность)​

Всё, что может быть уникально идентифицировано. Например:

from dataclasses import dataclass, field
from uuid import uuid4

class User:
user_id: str = field(default_factory=lambda: str(uuid4()))

Здесь User является сущностью, поскольку может быть уникально идентифицирован через user_id. Это означает, что для любых двух экземпляров User, u1 и u2, если u1.user_id == u2.user_id, то u1 == u2.

URI​

Uniform Resource Identifier (Унифицированный идентификатор ресурса)​

Строка, которая уникально идентифицирует ресурс. URI может быть URL, URN или и тем, и другим. URL следует формату:

protocol://domain/path?query#fragment

#fragment обычно используется для навигации на стороне клиента, обычно он не нужен при написании серверного приложения.

Пример:

https://myhost.com/users/lhl/orders?nums=3

Когда вы видите RESTful API с URI подобным этому, даже без предварительных знаний, вы можете сделать вывод, что:

  • Это веб-сайт, размещённый на myhost.com, использующий протокол https.
  • Он обращается к ресурсу с именем orders, который принадлежит конкретному пользователю lhl.
  • Он включает параметр запроса, nums, со значением 3.

URL (Uniform Resource Locator - Унифицированный указатель ресурса): Тип URI, который не только идентифицирует ресурс, но и предоставляет способ доступа к нему. URL обычно включают схему (протокол), домен, путь, параметры запроса и, опционально, фрагмент.

ASGI​

ASGI означает Asynchronous Server Gateway Interface (Асинхронный интерфейс шлюза сервера), протокол, разработанный encode.

ASGIApp​

это асинхронный вызываемый объект со следующей сигнатурой.

class ASGIApp(Protocol):
async def __call__(self, scope, receive, send) -> None: ...

где

  • scope это изменяемое отображение, часто dict.
  • receive это асинхронный вызываемый объект, который не имеет параметров и возвращает message
  • message также является изменяемым отображением, часто dict.
  • send это асинхронный вызываемый объект, который принимает единственный параметр message и возвращает None

Многие компоненты, которые вы видите в lihil, реализуют ASGIApp, включая

  • Lihil
  • Route
  • Endpoint
  • Response

asgi промежуточное ПО (middleware) также являются ASGIApp.

ASGI Call Chain (Цепочка вызовов ASGI)​

ASGIApp часто связываются вместе как связанный список (вы можете узнать это как паттерн chain of responsibility - цепочка ответственности), где каждый вызов цепочки проходит через каждый узел цепочки, например, обычный стек вызовов выглядит так:

  • Endpoint Function это функция, которую вы зарегистрировали с помощью Route.{http method}, такой как Route.get

  • Если вы обслуживаете lihil с помощью ASGI сервера, такого как uvicorn, lihil.Lihil будет вызван uvicorn.

Inversion of Control & Dependency Injection (Инверсия управления и внедрение зависимостей)​

Inversion of Control (Инверсия управления)​

Хотя термин Inversion of Control (Инверсия управления) может звучать экзотично и причудливо, и может интерпретироваться на разных уровнях проектирования программного обеспечения, мы говорим здесь только об одном из его узких смыслов.

Представьте, что вы пишете модуль, который создаёт пользователя в базе данных, у вас может быть что-то вроде:

from sqlalchemy.ext.asyncio import create_engine, AsyncEngine

class Repo:
def __init__(self, engine: AsyncEngine):
self.engine = engine

async def add_user(self, user: User):
prepared_stmt = sle.prepare_add_user(user)
async with self.engine.connct() as conn:
await conn.execute(prepared_stmt)

async def create_user(user: User, repo: Repository):
await repo.add_user(user)

async def main():
engine = create_engine(url=url)
repo = Repo(engine)
user = User(name="user")
await create_user(user, repo)

Здесь вы вызываете create_user из вашей функции main внутри main.py, когда вы вводите python -m myproject.main, функции main и create_user вызываются.

Сравните с тем, что вы бы делали с lihil:

users_route = Route("/users")

@users_route.post
async def create_user(user: User, repo: Repository):
await repo.add_user(user)

lhl = Lihil(user_route)

Обратите внимание, что здесь, вместо того чтобы вы активно вызывали create_user из вашей функции, ваша функция вызывается lihil при поступлении запроса, а зависимости вашей create_user управляются и внедряются lihil.

Это пример Inversion of Control (Инверсии управления) и также одна из основных причин, почему специальный инструмент внедрения зависимостей ididi используется lihil.

Сравнение с ручным построением зависимости внутри функции endpoint​

Вы можете задаться вопросом, почему бы не строить зависимости самостоятельно внутри функции endpoint.

@users_route.post
async def create_user(user: User):
engine = create_engine(url=url)
repo = Repo(engine)
await repo.add_user(user)

Поскольку мы не внедряем динамически Repo в create_user, мы теряем преимущества:

  • разделения интерфейса и реализации:

    1. часто мы хотим строить движок по-разному в зависимости от среды, в которой мы развёртываем наше приложение, например, вы можете захотеть увеличить размер пула соединений в продакшене.
    2. мы не будем выполнять реальные запросы во время тестирования, поэтому нам нужно будет замокать объект Engine.
    3. если мы создадим новый AdvancedEngine(Engine) для удовлетворения наших бизнес-потребностей, мы не сможем заставить create_user использовать его без изменения кода внутри.
  • контроля времени жизни: Зависимости имеют разное время жизни, например, вы можете захотеть повторно использовать один и тот же AsyncEngine для разных запросов, но открывать новое AsyncConnection для обработки каждого запроса.