Мини-курс
мы пройдём через некоторые из основных концепций, связанных с веб-разработкой, которые помогут вам лучше понять наши руководства.
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это асинхронный вызываемый объект, который не имеет параметров и возвращаетmessagemessageтакже является изменяемым отображением, частоdict.sendэто асинхронный вызываемый объект, который принимает единственный параметрmessageи возвращаетNone
Многие компоненты, которые вы видите в lihil, реализуют ASGIApp, включая
LihilRouteEndpointResponse
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, мы теряем преимущества:
-
разделения интерфейса и реализации:
- часто мы хотим строить движок по-разному в зависимости от среды, в которой мы развёртываем наше приложение, например, вы можете захотеть увеличить размер пула соединений в продакшене.
- мы не будем выполнять реальные запросы во время тестирования, поэтому нам нужно будет замокать объект
Engine. - если мы создадим новый
AdvancedEngine(Engine)для удовлетворения наших бизнес-потребностей, мы не сможем заставитьcreate_userиспользовать его без изменения кода внутри.
-
контроля времени жизни: Зависимости имеют разное время жизни, например, вы можете захотеть повторно использовать один и тот же
AsyncEngineдля разных запросов, но открывать новоеAsyncConnectionдля обработки каждого запроса.