Pythonのファクトリメソッドとインジェクトフレームワーク-何がよりクリーンですか?


9

私のアプリケーションで通常行うことは、ファクトリメソッドを使用してすべてのサービス/ dao / repo /クライアントを作成することです

class Service:
    def init(self, db):
        self._db = db

    @classmethod
    def from_env(cls):
        return cls(db=PostgresDatabase.from_env())

アプリを作成するとき

service = Service.from_env()

すべての依存関係を作成するもの

テストで実際のデータベースを使用したくない場合は、DIを実行します

service = Service(db=InMemoryDatabse())

サービスはデータベースの作成方法を知っており、どのデータベースタイプを作成するか(InMemoryDatabseまたはMongoDatabaseでもかまいません)を知っているため、これはクリーン/ 16進数アーキテクチャとはかなり異なると思います。

私はクリーン/ 16進アーキテクチャでは私が持っていると思います

class DatabaseInterface(ABC):
    @abstractmethod
    def get_user(self, user_id: int) -> User:
        pass

import inject
class Service:
    @inject.autoparams()
    def __init__(self, db: DatabaseInterface):
        self._db = db

そして私はインジェクターフレームワークを設定して

# in app
inject.clear_and_configure(lambda binder: binder
                           .bind(DatabaseInterface, PostgresDatabase()))

# in test
inject.clear_and_configure(lambda binder: binder
                           .bind(DatabaseInterface, InMemoryDatabse()))

そして私の質問は:

  • 私のやり方は本当に悪いのですか?それはもうクリーンなアーキテクチャではありませんか?
  • インジェクトを使用する利点は何ですか?
  • わざわざインジェクトフレームワークを使用する価値はありますか?
  • ドメインを外部から分離する他のより良い方法はありますか?

回答:


1

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ます。

このアプローチの詳細については、こちらをご覧ください。


0

最初の例は、「適切な」クリーン/ヘクスにかなり近いです。不足しているのはコンポジションルートの考え方であり、インジェクターフレームワークがなくてもクリーン/ヘクスを実行できます。それがなければ、あなたは次のようなことをするでしょう:

class Service:
    def __init__(self, db):
        self._db = db

# In your app entry point:
service = Service(PostGresDb(config.host, config.port, config.dbname))

誰と話すかに応じて、Pure / Vanilla / Poor ManのDIによって決まります。ダックタイピングや構造タイピングに頼ることができるため、抽象的なインターフェースは絶対に必要というわけではありません。

DIフレームワークを使用するかどうかは、意見と好みの問題ですが、その道を進むことを選択した場合、考慮できるpunqのような注入する他のより簡単な代替手段があります。

https://www.cosmicpython.com/は、これらの問題を詳細に検討するための優れたリソースです。


0

別のデータベースを使用したい場合や、簡単な方法で柔軟に実行したい場合があるため、依存関係の注入がサービスを構成するためのより良い方法だと思います

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.