11

我通常在我的应用程序中做的是使用工厂方法创建我所有的服务/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())

我想这与干净/十六进制架构相去甚远,因为服务知道如何创建数据库并知道它创建的数据库类型(也可能是 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()))

我的问题是:

  • 我的方式真的很糟糕吗?它不再是一个干净的架构了吗?
  • 使用注入有什么好处?
  • 是否值得打扰和使用注入框架?
  • 还有其他更好的方法可以将域与外部分开吗?
4

3 回答 3

5

依赖注入技术有几个主要目标,包括(但不限于):

  • 降低系统各部分之间的耦合。这样,您可以轻松地更改每个部分。参见“高内聚,低耦合”
  • 执行更严格的责任规则。一个实体必须在其抽象级别上只做一件事。必须将其他实体定义为对此实体的依赖项。见“国际奥委会”
  • 更好的测试体验。显式依赖项允许您使用与生产代码具有相同公共 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.

在此处阅读有关此方法的更多信息:

于 2020-02-03T20:41:13.050 回答
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/是一个很好的资源,可以深入研究这些问题。

于 2020-01-29T08:59:12.333 回答
0

您可能想使用不同的数据库,并且希望以简单的方式灵活地完成它,因此,我认为依赖注入是配置服务的更好方法

于 2020-02-02T17:30:16.773 回答