Фабричные методы против инъекционного фреймворка в Python - что чище?


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()

что создает все зависимости

и в тестах, когда я не хочу использовать реальный БД, я просто делаю DI

service = Service(db=InMemoryDatabse())

Я предполагаю, что это довольно далеко от чистой / шестнадцатеричной архитектуры, поскольку Service знает, как создать базу данных и знает, какой тип базы данных она создает (может быть также InMemoryDatabse или MongoDatabase)

Я думаю, что в чистой / шестнадцатеричной архитектуре я бы

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

В технике внедрения зависимостей есть несколько основных целей, включая (но не ограничиваясь ими):

  • Понижение связи между частями вашей системы. Таким образом, вы можете изменить каждую часть с меньшими усилиями. Смотрите «Высокая когезия, низкая связь»
  • Для обеспечения более строгих правил об ответственности. Одна сущность должна делать только одну вещь на своем уровне абстракции. Другие сущности должны быть определены как зависимости от этого. Смотрите "IoC"
  • Лучший опыт тестирования. Явные зависимости позволяют вам заглушить разные части вашей системы с помощью некоторого примитивного поведения тестирования, которое имеет тот же открытый API, что и ваш производственный код. Смотрите "Мок не заглушки"

Еще одна вещь, которую нужно иметь в виду, это то, что мы обычно полагаемся на абстракции, а не на реализации. Я вижу много людей, которые используют 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

Начальный пример довольно близок к «правильной» очистке / гексу. Чего не хватает, так это идеи Composition Root, и вы можете делать clean / hex без какой-либо структуры инжектора. Без этого вы бы сделали что-то вроде:

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

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

который зависит от DI Pure / Vanilla / Poor Man, в зависимости от того, с кем вы разговариваете. Абстрактный интерфейс не является абсолютно необходимым, так как вы можете положиться на типизацию утки или структурную типизацию.

Независимо от того, хотите ли вы использовать DI-фреймворк, это вопрос мнения и вкуса, но есть и другие более простые альтернативы, такие как punq, которые вы можете рассмотреть, если решите пойти по этому пути.

https://www.cosmicpython.com/ - хороший ресурс, в котором подробно рассматриваются эти проблемы.


0

Возможно, вы захотите использовать другую базу данных и хотите иметь возможность сделать это простым способом, поэтому я считаю внедрение зависимостей лучшим способом настройки вашей службы.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.