はじめに
DIをflaskで使う方法を記事にしました。
本記事ではflask-injectorを使用します。
併せて、DI・DIコンテナについても説明していきます。
DIとは?
まずは前提知識となるDIについて説明をしていきます。
Dependency Injectionは依存性の注入と訳されますが、
依存性の注入では何を表現しているのか?が掴みづらいと思います。
この依存性を言い換えるなら、依存関係にある要素・オブジェクトです。
「依存関係にあるオブジェクトを注入する」ことがDIパターンとなります。
DIパターンを使用することの利点
依存関係にあるオブジェクトを外部から注入することにより、
依存元のモジュールから依存しているオブジェクトを外部に出すことができます。
つまり、モジュール結合度を弱くすることができます。
DIパターンを使用していない場合
モジュール結合度を弱くすることで保守性の強いソースコードを作ることができます。
例えば、下記のようなコードがあるとします。
from dataclasses import dataclass
@dataclass
class Cat:
name: str
class Pet:
def call_name(self) -> None:
cat_cls = Cat("hoge")
print(cat_cls.name)
if __name__ == "__main__":
pet = Pet()
pet.call_name()
Petクラスの内部でCatクラスのインスタンスを生成しています。
PetクラスはCatクラスに強く依存している状態です。
依存状態が強いと、何が問題なのか?
上記を説明するためにCatクラスのインスタンス変数に変更を加えます。
@dataclass
class Cat:
name: str
age: int
変更点として、Catクラスのインスタンス変数にageという変数を追加しました。
変更はCatクラスに行ったものですが、
PetクラスでCatクラスのインスタンスを生成しています。
Petクラスでも修正が必要になるのです。
def call_name(self) -> None:
cat_cls = Cat("hoge") # <-- この箇所
print(cat_cls.name)
下記ではモジュールが少ないのでイメージが付きづらいと思います。
極論にはなりますが、100個のモジュールがCatクラスに依存しているとします。
その場合、依存している100個のモジュール全てを修正していく必要があります。
変更したかったのはCatクラスのインスタンス変数であるにも関わらずです。
このようにモジュール結合度が高いと、変更に弱いソースコードになってしまいます。
DIパターンを使用する場合
ここでDIパターンを使用していきます。
CatクラスをPetクラスの中で呼び出すのではなく、
外部から注入します。
from dataclasses import dataclass
@dataclass
class Cat:
name: str
age: int
class Pet:
def __init__(self, cat: Cat):
self.cat = cat
def call_name(self) -> None:
print(self.cat.name)
if __name__ == "__main__":
cat = Cat("taro", 3)
pet = Pet(cat)
pet.call_name()
こうすることてCatクラスに変更があった場合でも
修正箇所を特定しやすくできます。
※依存関係においてはSOLID原則の依存性逆転の原則などもあります。
取り扱っている事柄などが似通っていますが、
DIパターンと依存性逆転の原則は別のものです。
DIコンテナについて
DIコンテナはDIを使用する上では必須ではありません。
複数のインスタンスを渡すときに呼び出しが煩雑になってきます。
インスタンスを管理する場所が欲しくなります。
そこでDIコンテナを使用しています。
DIコンテナの使用用途は煩雑になるインスタンスの管理です。
flaskでDIを使用する
ここからが本題です。
今回はFlaskにInjectorを追加するため、Flask-Injectorを使用していきます。
Flask-Injector使用時にはFlaskのバージョンも確認しておく必要があります。
PyPiのFlask-Injectorから引用
Flask-Injector is compatible with CPython 3.7+. As of version 0.15.0 it requires Injector version 0.20.0 or greater and Flask 2.2.0 or greater.
注意
なお、バージョン0.15.0以降のFlask-Injectorを使用する場合はInjectorのバージョンが0.20.0であることFlaskのバージョンが2.2.0以上である必要があります。
pip install Flask-Injector
Flask-Injectorをインストールできたら、
コードを書き進めていきます。
Flask-Injectorは内部でInjectorモジュールを使用しております。
実際に使う場合にはInjectorモジュールの学習も必要になります。
かなり簡易的な実装になります。
そちらを留意頂けると幸いでございます。
from flask import Flask
from flask_injector import FlaskInjector, request, Binder, inject
from model import AbstractUserProfileTable, UserProfileTable
from dataclasses import dataclass
app = Flask(__name__)
@dataclass
class Hoge:
name: str = "taro"
class Fuga:
@inject
def __init__(self, hoge: Hoge):
self.hoge = hoge
def test_func(self):
return self.hoge.name
@app.route("/foo")
def foo(test_cls: Fuga):
return test_cls.test_func()
@app.route("/bar")
def bar(user_profile: AbstractUserProfileTable):
users = user_profile.find_all()
return users
def configure(binder: Binder):
binder.bind(
AbstractUserProfileTable,
to=UserProfileTable,
scope=request
)
injecter = FlaskInjector(app=app, modules=[configure])
if __name__ == "__main__":
app.run()
import os
from dotenv import load_dotenv
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.schema import Column
from sqlalchemy.types import Integer, String
from abc import ABC, abstractmethod
from sqlalchemy.orm import sessionmaker
load_dotenv()
engine = create_engine(
f"postgresql+psycopg2://{os.getenv('USER')}:{os.getenv('PASSWORD')}@{os.getenv('HOST')}:{os.getenv('PORT')}/{os.getenv('DATABASE')}")
Base = declarative_base()
session_maker = sessionmaker(engine)
session = session_maker()
class UserProfile(Base):
__tablename__ = "user_profile"
user_id = Column(Integer, primary_key=True)
first_name = Column(String)
last_name = Column(String)
class AbstractUserProfileTable(ABC):
@abstractmethod
def find_all(self):
pass
class UserProfileTable(AbstractUserProfileTable):
def find_all(self):
return session.query(UserProfile).all()
コードの解説をしていきます。
class Fuga:
@inject
def __init__(self, hoge: Hoge):
self.hoge = hoge
外部からオブジェクトを注入する際には@injectでデコレートする必要があります。
@injectでデコレートしないとFugaクラスのインスタンス生成時にエラーになります。
また、型アノテーションが必須になるので忘れずに宣言しておいてください。
binder.bind(
AbstractUserProfileTable,
to=UserProfileTable,
scope=request
)
上記のコードは抽象クラスと具象クラスの紐づけに使用しています。
@app.route("/bar")
def bar(user_profile: AbstractUserProfileTable):
users = user_profile.find_all()
return users
上記ではAbstractUserProfileTableインスタンスを注入しているが、呼びされているfind_allメソッドは具象クラスのメソッドになります。
参考にさせていただいた資料・記事