PythonのDI(Dependency Injection)ライブラリInjectorとSpringの比較
背景
最近、Pythonを触る機会があり、オニオンアーキテクチャを意識してコードを書いているのですが、DI(Dependency Injection)を適用したくなってきました。普段仕事では、Java+Springで開発しているので、DIコンテナは意識せずとも利用しています。PythonでもDIコンテナのライブラリがあるのか調べてみたところ、Injectorというライブラリを紹介している人が多そうに思ったので、今回はこれを使ってみることにしました。使い方について、理解するまで少々戸惑ってしまったのですが、普段Springを触っている私にとってはSpringと対応させて考えると理解しやすいと思ったのでメモとして残しておきます。
@Autowired
Springではインジェクションする対象に @Autowired
というアノテーションを付与します。Springではインジェクションの方法は以下の3種類があります。
- コンストラクタインジェクション
- フィールドインジェクション
- セッターインジェクション
例えば、コンストラクタインジェクションではコンストラクタに @Autowired
というアノテーションをつけます。
class MyService {
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
Injectorでは、コンストラクタインジェクションを行う場合は __init__
メソッドに @inject
というデコレータをつけます。
from injector import inject
class MyService:
@inject
def __init__(self, my_repository: MyRepository):
self.my_repository = my_repository
セッターインジェクション
コード例をどこで見たか分からなくなってしまいましたが、Injectorではメソッドに対しても @inject
というデコレータをつけることでインジェクションすることができるようです。これが正しいとすると、これを利用してSpringでいうセッターインジェクションと同等のことができそうです。ただ、基本的にはコンストラクタインジェクションで事足りるのではないかと思います。
Injectorでフィールドインジェクションが可能かは分かりませんでした。
Beanの登録
Springでは、DIコンテナが管理しているオブジェクトのことをBeanと呼びます。Beanに登録する方法は以下の3種類があります。
- ステレオタイプアノテーション
- @Beanメソッド
- <bean>タグ(ここでは割愛)
ステレオタイプアノテーション
ステレオタイプアノテーションとは、 @Component
などのアノテーションをクラスに付与することで、コンポーネントスキャンの対象となり、DIコンテナが管理するオブジェクトとして扱われるというものです。
from injector import Injector, inject
class Inner:
def __init__(self):
self.forty_two = 42
class Outer:
@inject
def __init__(self, inner: Inner):
self.inner = inner
injector = Injector()
outer = injector.get(Outer)
outer.inner.forty_two
Injectの場合、公式のREADMEに記載されている上記のサンプルを見ても、Innerクラスには @inject
や @provider
などのInject固有の特別なことはしていませんが、Outerクラスの __init__
から必要とされる際に、Innerクラスのインスタンスが生成されることが分かります。Springと異なりステレオタイプアノテーションや@Beanメソッドで明示的にBean登録せずともインジェクションが行われるようです。
@Beanメソッド
@Bean
public MyService myService(MyRepository myRepository) {
return new MyService(myRepository);
}
@BeanメソッドはJavaConfigクラス(@Configurationを付与したクラス)内で定義され、このメソッドをDIコンテナが自動的に呼び出し、返したオブジェクトがBeanとして管理されるというものです。
Injectの場合、ModuleがJavaConfigクラスに対応するものであり、 @provider
が @Bean
に対応するものであると理解しています。
class RequestHandler:
@inject
def __init__(self, db: sqlite3.Connection):
self._db = db
def get(self):
cursor = self._db.cursor()
cursor.execute('SELECT key, value FROM data ORDER by key')
return cursor.fetchall()
class DatabaseModule(Module):
@singleton
@provider
def provide_sqlite_connection(self, configuration: Configuration) -> sqlite3.Connection:
conn = sqlite3.connect(configuration.connection_string)
cursor = conn.cursor()
cursor.execute('CREATE TABLE IF NOT EXISTS data (key PRIMARY KEY, value)')
cursor.execute('INSERT OR REPLACE INTO data VALUES ("hello", "world")')
return conn
injector = Injector([configure_for_testing, DatabaseModule()]) # DatabaseModuleを利用
handler = injector.get(RequestHandler)
tuple(map(str, handler.get()[0]))
RequestHandlerは __init__
に @inject
が付与されており、sqlite3.Connection
に依存していることが示されているため、DIコンテナはこれを注入しようとします。この際、sqlite3.Connection
を返す @provider
が付与されたメソッドが存在するため、DIコンテナはこれを利用してオブジェクトを生成します。このとき、DIコンテナは @provider
が付与されたメソッドの戻り値の型を見て判断していると思われます。また、Springの@Beanメソッドの場合は、デフォルトではメソッド名がBean名になりますが、 @provider
の場合はメソッド名は特に意味をなさずプログラマが任意につけることができるのではないかと思っています。
さらに、@singleton
が付与されているため、injector.get(RequestHandler)
が複数回呼び出されても複数回オブジェクトを生成することはせず、同一のオブジェクトが返されます。
injector.get(sqlite3.Connection) is injector.get(sqlite3.Connection) # -> True
インターフェースと実装クラスの紐付け
Springでは、インターフェースに対してBean登録されている実装クラスが1つしかない場合、自動的にインターフェースと実装クラスの紐付けを行います。
public interface MyRepository {
void save(String value);
}
@Repository
public class MyRepositoryImpl implements MyRepository {
@Override
public void save(String value) {
// 実装
}
}
@Service
public class MyService {
private final MyRepository myRepository;
// Bean登録されているMyRepositoryの実装クラスがMyRepositoryImplしか存在しないため、
// 設定などは書かなくてもmyRepositoryにはMyRepositoryImplのインスタンスが注入される
// @Autowired コンストラクタが1つの場合は省略可能
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
Injectorでは、このような紐付けは自動では行われないため、Binder
を利用して紐付けの設定を行う必要があるようです。
from abc import abstractmethod, ABC
from injector import Binder, Module, Injector
class MyRepository(ABC):
@abstractmethod
def save(self, value: str):
pass
class MyRepositoryImpl(MyRepository):
def save(self, value: str):
print(f"Saving {value}")
class MyModule(Module):
def configure(self, binder: Binder):
binder.bind(MyRepository, to=MyRepositoryImpl)
injector = Injector([MyModule()])
my_repository = injector.get(MyRepository)
my_repository.save("some value") # -> "Saving some value"
以下のようにModuleを渡さずに実行するとエラーが発生する
injector = Injector()
my_repository = injector.get(MyRepository)
injector.CallError: Call to ABCMeta.__new__() failed: Can't instantiate abstract class MyRepository without an implementation for abstract method 'save' (injection stack: [])
まとめ
自分の頭の整理のために、Springの知識を元にして、PythonのInjectorの挙動を理解してみました。Injectorについて、まだ理解が足りないところも多々ありそうですが、本エントリを書くことで頭の整理にはなったかなと思います。
参考サイト
Discussion