Dependency Injection手法には、次のようないくつかの主要な目標があります(これらに限定されません)。
- システムのパーツ間の結合を下げる。このようにして、より少ない労力で各パーツを変更できます。「高凝集性、低結合」を参照してください
- 責任に関するより厳しい規則を実施するため。1つのエンティティは、その抽象化のレベルで1つのことだけを実行する必要があります。他のエンティティは、このエンティティへの依存関係として定義する必要があります。「IoC」を参照
- より良いテスト経験。明示的な依存関係により、本番コードと同じパブリックAPIを持ついくつかの基本的なテスト動作でシステムのさまざまな部分をスタブ化できます。「モックアレントのスタブ」を参照
覚えておくべきもう1つのことは、通常は実装ではなく抽象化に依存することです。DIを使用して特定の実装のみを注入する多くの人がいます。大きな違いがあります。
実装を注入して依存する場合、オブジェクトの作成に使用するメソッドに違いはないためです。それは問題ではありません。たとえば、requests
適切な抽象化なしで注入する場合でも、同じメソッド、シグネチャ、および戻り値の型で同様のものが必要になります。この実装を置き換えることはできません。しかし、あなたが注入fetch_order(order: OrderID) -> Order
するとき、それは何かが中にあることができることを意味します。requests
、データベース、なんでも。
まとめると:
インジェクトを使用する利点は何ですか?
主な利点は、依存関係を手動でアセンブルする必要がないことです。ただし、これには莫大なコストが伴います。問題を解決するために、複雑で魔法のようなツールを使用しています。いつの日か別の複雑さがあなたを撃退します。
わざわざインジェクトフレームワークを使用する価値はありますか?
inject
特にフレームワークについてもう1つ。何かを注入するオブジェクトがそれについて知っているときは気に入らない。実装詳細です!
Postcard
たとえば、ワールドドメインモデルでは、このことをどのように知っていますか?
punq
単純なケースとdependencies
複雑なケースに使用することをお勧めします。
inject
また、「依存関係」とオブジェクトプロパティの明確な分離は強制されません。言われたように、DIの主な目標の1つは、より厳密な責任を強制することです。
対照的に、私はどのようにpunq
機能するかを示しましょう:
from typing_extensions import final
from attr import dataclass
# Note, we import protocols, not implementations:
from project.postcards.repository.protocols import PostcardsForToday
from project.postcards.services.protocols import (
SendPostcardsByEmail,
CountPostcardsInAnalytics,
)
@final
@dataclass(frozen=True, slots=True)
class SendTodaysPostcardsUsecase(object):
_repository: PostcardsForToday
_email: SendPostcardsByEmail
_analytics: CountPostcardInAnalytics
def __call__(self, today: datetime) -> None:
postcards = self._repository(today)
self._email(postcards)
self._analytics(postcards)
見る?コンストラクタもありません。依存関係を宣言的に定義し、punq
それらを自動的に挿入します。また、特定の実装は定義していません。従うべきプロトコルのみ。このスタイルは「機能オブジェクト」またはSRPスタイルのクラスと呼ばれます。
次に、punq
コンテナ自体を定義します。
# project/implemented.py
import punq
container = punq.Container()
# Low level dependencies:
container.register(Postgres)
container.register(SendGrid)
container.register(GoogleAnalytics)
# Intermediate dependencies:
container.register(PostcardsForToday)
container.register(SendPostcardsByEmail)
container.register(CountPostcardInAnalytics)
# End dependencies:
container.register(SendTodaysPostcardsUsecase)
そしてそれを使う:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
見る?現在、私たちのクラスは誰がどのようにそれらを作成するのかわかりません。デコレータなし、特別な値なし。
SRPスタイルのクラスの詳細については、こちらをご覧ください。
ドメインを外部から分離する他のより良い方法はありますか?
命令型の概念の代わりに関数型プログラミングの概念を使用できます。関数の依存性注入の主なアイデアは、持っていないコンテキストに依存するものを呼び出さないことです。これらの呼び出しは、コンテキストが存在するときに後でスケジュールします。単純な関数で依存性注入を説明する方法は次のとおりです。
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from words_app.logic import calculate_points
def view(request: HttpRequest) -> HttpResponse:
user_word: str = request.POST['word'] # just an example
points = calculate_points(user_words)(settings) # passing the dependencies and calling
... # later you show the result to user somehow
# Somewhere in your `word_app/logic.py`:
from typing import Callable
from typing_extensions import Protocol
class _Deps(Protocol): # we rely on abstractions, not direct values or types
WORD_THRESHOLD: int
def calculate_points(word: str) -> Callable[[_Deps], int]:
guessed_letters_count = len([letter for letter in word if letter != '.'])
return _award_points_for_letters(guessed_letters_count)
def _award_points_for_letters(guessed: int) -> Callable[[_Deps], int]:
def factory(deps: _Deps):
return 0 if guessed < deps.WORD_THRESHOLD else guessed
return factory
このパターンの唯一の問題は、_award_points_for_letters
構成が難しいことです。
我々はそれがの一部である(組成を支援するための特別なラッパーを作った理由ですreturns
。
import random
from typing_extensions import Protocol
from returns.context import RequiresContext
class _Deps(Protocol): # we rely on abstractions, not direct values or types
WORD_THRESHOLD: int
def calculate_points(word: str) -> RequiresContext[_Deps, int]:
guessed_letters_count = len([letter for letter in word if letter != '.'])
awarded_points = _award_points_for_letters(guessed_letters_count)
return awarded_points.map(_maybe_add_extra_holiday_point) # it has special methods!
def _award_points_for_letters(guessed: int) -> RequiresContext[_Deps, int]:
def factory(deps: _Deps):
return 0 if guessed < deps.WORD_THRESHOLD else guessed
return RequiresContext(factory) # here, we added `RequiresContext` wrapper
def _maybe_add_extra_holiday_point(awarded_points: int) -> int:
return awarded_points + 1 if random.choice([True, False]) else awarded_points
たとえば、純粋な関数で構成するためのRequiresContext
特別な.map
メソッドがあります。以上です。その結果、シンプルなAPIを備えたシンプルな関数とコンポジションヘルパーだけが得られます。魔法も余分な複雑さもありません。ボーナスとして、すべてが適切に入力され、と互換性がありmypy
ます。
このアプローチの詳細については、こちらをご覧ください。