파이썬에서 팩토리 메소드 대 프레임 워크 주입-더 깨끗한 것은 무엇입니까?


9

응용 프로그램에서 일반적으로하는 일은 팩토리 메소드를 사용하여 모든 서비스 / dao / repo / clients를 작성하는 것입니다

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

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

그리고 앱을 만들면

service = Service.from_env()

모든 의존성을 만드는 것

그리고 실제 db를 사용하고 싶지 않을 때 테스트에서 DI를 수행합니다.

service = Service(db=InMemoryDatabse())

Service가 데이터베이스를 만드는 방법을 알고 데이터베이스가 생성하는 데이터베이스 유형을 알기 때문에 Clean / Hex 아키텍처와는 거리가 멀다고 생각합니다 (InMemoryDatabse 또는 MongoDatabase 일 수도 있음).

깨끗하고 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 기술에는 다음을 포함하여 몇 가지 주요 목표가 있습니다.

  • 시스템 부품 간의 결합을 낮추십시오. 이렇게하면 적은 노력으로 각 부품을 변경할 수 있습니다. 참조 "높은 응집력, 낮은 커플 링"
  • 책임에 대한보다 엄격한 규칙을 시행합니다. 한 엔티티는 추상화 레벨에서 한 가지만 수행해야합니다. 다른 엔티티는이 엔티티에 대한 종속성으로 정의되어야합니다. "IoC" 참조
  • 더 나은 테스트 경험. 명시 적 종속성을 사용하면 프로덕션 코드와 동일한 공용 API를 가진 일부 원시 테스트 동작으로 시스템의 다른 부분을 스텁 할 수 있습니다. 참조 "모의 객체는이 arent '스텁"

명심해야 할 또 다른 사항은 일반적으로 구현이 아닌 추상화에 의존한다는 것입니다. DI를 사용하여 특정 구현 만 주입하는 많은 사람들이 있습니다. 큰 차이가 있습니다.

구현을 주입하고 의존 할 때 객체를 만드는 데 사용하는 방법에는 차이가 없기 때문입니다. 중요하지 않습니다. 예를 들어, requests적절한 추상화없이 주입하는 경우 에도 동일한 메소드, 서명 및 리턴 유형과 유사한 것이 필요합니다. 이 구현을 전혀 대체 할 수는 없습니다. 그러나 주사 fetch_order(order: OrderID) -> Order하면 내부에 무엇이든있을 수 있음을 의미합니다. requests, 데이터베이스 등

요약하면 다음과 같습니다.

주입을 사용하면 어떤 이점이 있습니까?

주요 이점은 종속성을 수동으로 조립할 필요가 없다는 것입니다. 그러나 비용이 많이 듭니다. 문제를 해결하기 위해 복잡하고 마술적인 도구를 사용하고 있습니다. 언젠가는 또 다른 복잡성이 당신을 물리 칠 것입니다.

주입 프레임 워크를 귀찮게하고 사용하는 것이 가치가 있습니까?

inject특히 프레임 워크에 대해 한 가지 더 . 나는 무언가를 주입 한 물체가 그것에 대해 알고있는 것을 좋아하지 않습니다. 구현 세부 사항입니다!

Postcard예를 들어 월드 도메인 모델에서이 사실을 어떻게 알 수 있습니까?

punq간단한 경우와 dependencies복잡한 경우 에 사용 하는 것이 좋습니다 .

inject또한 "종속성"과 개체 속성을 완전히 분리하지 않습니다. 말했듯이 DI의 주요 목표 중 하나는 더 엄격한 책임을 수행하는 것입니다.

반대로, 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

초기 예제는 "적절한"정리 / 육각에 가깝습니다. 누락 된 것은 컴포지션 루트의 아이디어이며 인젝터 프레임 워크없이 깨끗하고 16 진수를 수행 할 수 있습니다. 그것 없이는 다음과 같은 일을 할 것입니다.

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 's DI가 적용됩니다. 오리 타이핑 또는 구조적 타이핑에 의존 할 수 있으므로 추상 인터페이스가 반드시 필요한 것은 아닙니다.

DI 프레임 워크를 사용할지 여부는 의견과 취향의 문제이지만, 그 길을 가고자한다면 고려할 수있는 펑크와 같은 다른 간단한 대안이 있습니다.

https://www.cosmicpython.com/ 은 이러한 문제를 자세히 살펴볼 수있는 유용한 리소스입니다.


0

다른 데이터베이스를 사용하고 간단한 방법으로 유연성을 갖기를 원할 수 있습니다.이 때문에 종속성 주입이 서비스를 구성하는 더 좋은 방법이라고 생각합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.