依赖注入技术有几个主要目标,包括(但不限于):
- 降低系统各部分之间的耦合。这样,您可以轻松更改每个零件。参见“高内聚,低耦合”
- 实施更严格的责任规则。一个实体只能在其抽象级别上做一件事。必须将其他实体定义为对此实体的依赖。参见“ 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
。
在此处阅读有关此方法的更多信息: