ミニコース
チュートリアルをより良く理解するのに役立つWeb開発に関連する一般的な概念について学習します。
Resource リソース
URLを介してアクセスできる識別可能なエンティティ
考えすぎないでください。resourceという用語が気に入らない場合は、objectとして考えてください。
Entity エンティティ
一意に識別できるもの。例えば
from dataclasses import dataclass, field
from uuid import uuid4
class User:
user_id: str = field(default_factory=lambda: str(uuid4()))
ここで、Userはuser_idを通じて一意に識別できるためエンティティです。
つまり、任意の2つの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を見ると、事前知識がなくても以下を推測できます:
- これは
httpsプロトコルを使用してmyhost.comでホストされているWebサイトです。 - 特定のユーザー
lhlに属するordersという名前のリソースにアクセスしています。 - クエリパラメータ
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を実装しており、以下が含まれます
LihilRouteEndpointResponse
ASGIミドルウェアもASGIAppです。
ASGI呼び出しチェーン
ASGIAppは通常、連結リストのように連鎖され(責任の連鎖パターンとして認識されるかもしれません)、チェーンへの各呼び出しはチェーン上のすべてのノードを通過します。例えば、通常の呼び出しスタックは次のようになります
-
Endpoint FunctionはRoute.{http method}で登録した関数、例えばRoute.get -
uvicornなどのASGIサーバーでlihilを提供する場合、
lihil.Lihilはuvicornによって呼び出されます。
制御の反転 & 依存性注入
制御の反転
制御の反転という用語は異国的で派手に聞こえるかもしれませんし、ソフトウェア設計の異なるレベルで解釈できますが、ここではその狭義の意味の1つだけについて話します。
データベースでユーザーを作成するモジュールを書いていると想像してください。次のようなものがあるかもしれません:
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)
ここで、main.py内のmain関数からcreate_userを呼び出しています。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によって管理・注入されます。
これは制御の反転の例であり、lihilが専用の依存性注入ツールididiを使用する主な理由の1つでもあります。
エンドポイント関数内で依存関係を手動構築することとの比較
エンドポイント関数内で依存関係を自分で構築しない理由を疑問に思うかもしれません。
@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を開きたい場合があります。