Фабричные методы против структуры ввода в Python - что чище? - PullRequest
9 голосов
/ 25 января 2020

Что я обычно делаю в своих приложениях, так это то, что я создаю все свои сервисы / 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())

Я предполагаю, что это довольно далеко от чистой / шестнадцатеричной архитектуры, так как 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()))

И мои вопросы:

  • Мой путь действительно плох? Разве это не чистая архитектура?
  • Каковы преимущества использования инжекта?
  • Стоит ли беспокоиться и использовать каркас инъекций?
  • Существуют ли другие лучшие способы отделить домен снаружи?

Ответы [ 3 ]

1 голос
/ 03 февраля 2020

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

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

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

Потому что, когда вы внедряете и полагаетесь на реализацию, нет никакой разницы в том, какой метод мы используем для создания объектов. Это просто не имеет значения. Например, если вы вводите requests без надлежащих абстракций, вам все равно потребуется что-то похожее с теми же методами, сигнатурами и типами возвращаемых данных. Вы не сможете заменить эту реализацию вообще. Но когда вы вводите fetch_order(order: OrderID) -> Order, это означает, что внутри может быть что угодно. requests, база данных, что угодно.

Подводя итог:

Каковы преимущества использования инъекций?

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

Стоит ли беспокоиться и использовать инфраструктуру для инъекций?

Еще одна вещь, касающаяся inject инфраструктуры в частности. Мне не нравится, когда объекты, в которые я ввожу что-то, знают об этом. Это деталь реализации!

Как в мире, например, модель домена 1039 * знает эту вещь?

Я бы порекомендовал использовать 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 автоматически внедряет их. И мы не определяем никаких конкретных c реализаций. Только протоколы для подражания. Этот стиль называется "функциональными объектами" или классами в стиле 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, без магии c, без дополнительной сложности. И в качестве бонуса все правильно напечатано и совместимо с mypy.

Подробнее об этом подходе читайте здесь:

0 голосов
/ 02 февраля 2020

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

0 голосов
/ 29 января 2020

Исходный пример довольно близок к "правильной" очистке / гексу. Чего не хватает, так это идеи 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, который вы могли бы рассмотреть, если вы решите go по этому пути.

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

...