はじめに
PythonのDIフレームワークDependency Injector
のドキュメントが示唆に富んでいたので、多くの人にPythonにおけるDIの良さを知ってもらうために、その一部を訳しました。
PythonにおけるDI(依存性の注入)と制御の逆転
もともとDIパターンは、Javaのような静的型付け言語で人気がありました。DIは、制御の逆転を実現するのに役立つ原則です。DIフレームワークは、静的型付けを使用して言語の柔軟性を大幅に向上させることができます。静的型付けを使用する言語のDIレームワークの実装はすぐにできるものではありません。うまくやるのはかなり複雑なことになるでしょう。そして時間がかかります。
Pythonは、動的型付けを使用したインタープリター言語です。PythonにおけるDIはJavaのようにうまくはいかないという意見があります。多くの柔軟性がすでに組み込まれているからです。また、DIフレームワークはPython開発者がめったに必要としないものであるという意見もあります。 Python開発者は、DIは基本的な言語機能を使用して簡単に実装できると言います。
このページでは、PythonでのDIの使用の利点について説明します。ここではDIを実装する方法を示すPythonの例が含まれています。ここではDIフレームワークDependency Injector
とそのコンテナ、Factory
、Singleton
, Configuration
プロバイダの使い方をの使い方を説明します。この例は、さまざまな環境でプロジェクトをテストまたはコンフィギュレーションするためのDependency Injector
プロバイダーのオーバーライド機能を使用する方法を示し、モンキーパッチよりも優れている理由を説明します。
依存性の注入(DI)とはなにか?
依存性の注入(DI)とは何かを見ていきましょう。
DIは結合度(Coupling)を減らし、凝集度(Cohesion)を高めるのに役立つ原理です。
結合、凝集とは何か?
結合度と凝集度は、部品がどれだけ強く結ばれるかということを表しています。
-
高結合 結合度が高い場合は、瞬間接着剤や溶接を使用するようなものです。分解する簡単な方法はありません。
-
高凝集 凝集度が高い場合とは、ネジを使用するようなものです。分解して組み立て直す、または別の方法で組み立てるのは非常に簡単です。これは、高結合の反対です。
凝集度が高いと結合度は低くなります。
低結合は柔軟性をもたらします。コードの変更とテストが簡単になります。
DIを実装する方法は?
オブジェクトが互いを生成しないようにします。代わりに、依存性を注入する方法を提供します。
Before:
import os
class ApiClient:
def __init__(self):
self.api_key = os.getenv('API_KEY') # <-- 依存性
self.timeout = os.getenv('TIMEOUT') # <-- 依存性
class Service:
def __init__(self):
self.api_client = ApiClient() # <-- 依存性
def main() -> None:
service = Service() # <-- 依存性
...
if __name__ == '__main__':
main()
After:
import os
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key # <-- 依存性が注入された
self.timeout = timeout # <-- 依存性が注入された
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client # <-- 依存性が注入された
def main(service: Service): # <-- 依存性が注入された
...
if __name__ == '__main__':
main(
service=Service(
api_client=ApiClient(
api_key=os.getenv('API_KEY'),
timeout=os.getenv('TIMEOUT'),
),
),
)
ApiClient
は、オプションがどこから来たのかを知ることから切り離されています。(環境変数に限定されず)APIキーとタイムアウト時間をファイルから読み込んだり、データベースから取得したりすることもできます。
Service
はApiClient
から切り離されています。Service
はもうそれを内部で作成しません。スタブや他の互換性のあるオブジェクトを入れることができます。
柔軟性には代償が伴います。
次のようにオブジェクトを組み立てて注入する必要が出てきました。
main(
service=Service(
api_client=ApiClient(
api_key=os.getenv('API_KEY'),
timeout=os.getenv('TIMEOUT'),
),
),
)
この組み立てるコードが重複し、アプリケーションの構造を変更するのが難しくなる可能性があります。
ここでDependency Injector
の出番です。
Dependency Injectorは何をするのか?
DIパターンではオブジェクトは依存性を組み立てる責任を放棄します。Dependency Injector
はその責任を引き受けます。
Dependency Injector
は、依存性を組み立てて注入するのに役立ちます。
Dependency Injector
はオブジェクトの組み立てを支援するコンテナーとプロバイダーを提供します。オブジェクトが必要な場合は、関数の引数のデフォルト値としてProvide
マーカーを配置します。この関数を呼び出すと、フレームワークが依存関係を組み立てて注入します。
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
service = providers.Factory(
Service,
api_client=api_client,
)
@inject
def main(service: Service = Provide[Container.service]):
...
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
# コンテナを注入する対象のモジュールを指定する
container.wire(modules=[sys.modules[__name__]])
main() # <-- 依存性が自動的に注入される
with container.api_client.override(mock.Mock()):
main() # <-- モックに置き換えられた依存性が自動的に注入される
main()
関数を呼び出すと、Service
の依存関係が自動的に組み立てられ注入されます。
テストを行うときは、container.api_client.override()
を呼び出して、実際のAPIクライアントをモックに置き換えます。main()
を呼び出すと、モックが注入されます。
任意のプロバイダーを別のプロバイダーで置き換えることができます。
また、さまざまな環境向けにプロジェクトをを環境設定するのにも役立ちます。今回の例では、APIクライアントを開発環境もしくはステージング環境のスタブに置き換えています。
オブジェクトの組み立てはコンテナに統合されます。依存性の注入が明示的に定義されます。これにより、アプリケーションの動作を理解し、変更することが容易になります。
テスト、モンキーパッチ、DI
テスト容易性の利点は、モンキーパッチとは対照的です。
Pythonでは、いつでも何にでもモンキーパッチを適用できます。モンキーパッチの問題は、壊れやすいことです。その理由は、モンキーパッチを適用するときに、意図されていないことを実行するためです。実装の詳細な部分にモンキーパッチを適用してしまったとします。その実装が変更されると、モンキーパッチが壊れます。
依存性注入を使用すると、実装ではなく、インターフェースにパッチを適用します。これは、より安定したアプローチです。
また、モンキーパッチは、テストコードの外で異なる環境にあわせてプロジェクトを再設定するために使うにはあまりに汚すぎる方法です。
結論
DIには3つの利点があります。
-
柔軟性 コンポーネントは疎結合です。コンポーネントをさまざまな方法で組み合わせることで、システムの機能を簡単に拡張または変更できます。あなたもその場でそれを行うことができます。
-
テスト容易性 APIやデータベースなどを使用する実際のオブジェクトの代わりにモックを簡単に注入できるため、テストは簡単です。
-
明確さと保守性 DIは、依存性を明らかにするのに役立ちます。暗黙的だったものが明示的になります。「明示的は暗黙的よりも優れている」とPEP 20 - Zen of Pythonにも書いてあります。すべてのコンポーネントや依存性がコンテナ内で明示的に定義されます。これにより、アプリケーション構造の概要と制御が提供されます。理解しやすく、変更も簡単です。
PythonでDIを使用する価値はありますか?
それはあなたが何を構築するかに依存します。 Pythonをスクリプト言語として使用する場合、上記の利点はそれほど重要ではありません。 Pythonを使用してアプリケーションを作成すると、状況が異なります。アプリケーションが大きいほど、メリットは大きくなります。
DIフレームワークを使用する価値はありますか?
PythonでのDIパターンの実装の複雑さは、他の言語よりも低くなっていますが、それでも機能しています。フレームワークを使用する必要があるという意味ではありませんが、フレームワークを使用することは次のような点において有益です。
- すでに実装されている
- すべてのプラットフォームとバージョンでテストされている
- ドキュメントがある
- サポートがある
- 他のエンジニアも知っている
最後に少しアドバイス
-
やってみよう DIは直感に反します。私たちは普通、何かが必要なときに最初に頭に浮かぶのは、それを手に入れることです。そんな我々に対してDIは「待て、今すぐ何かを手に入れるのではなく、それがなぜ必要なのかを先に書け」と言ってきます。それは後で報われる小さな投資のようなものです。アドバイスは、2週間試してみることです。その時間は効果を実感するのに十分でしょう。それが気に入らなければ、少なくともそれ以上失うものは何もありません。
-
まず直感で考える DIを適応するには直感を働かせましょう。これはよい原則ですが、銀の弾丸ではありません。やりすぎれば実装の詳細をさらけ出してしまいます。経験には練習と時間が伴います。(これを期にDDDやオニオンアーキテクチャに手を付けてもいいでしょう)