Ala*_*cka 10 python architecture dependency-injection hexagonal-architecture clean-architecture
我通常在我的应用程序中做的是使用工厂方法创建我所有的服务/dao/repo/clients
class Service:
def init(self, db):
self._db = db
@classmethod
def from_env(cls):
return cls(db=PostgresDatabase.from_env())
Run Code Online (Sandbox Code Playgroud)
当我创建应用程序时
service = Service.from_env()
Run Code Online (Sandbox Code Playgroud)
什么创建了所有依赖项
在我不想使用真实数据库的测试中,我只做 DI
service = Service(db=InMemoryDatabse())
Run Code Online (Sandbox Code Playgroud)
我想这与干净/十六进制架构相去甚远,因为 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
Run Code Online (Sandbox Code Playgroud)
我会设置注入器框架来做
# in app
inject.clear_and_configure(lambda binder: binder
.bind(DatabaseInterface, PostgresDatabase()))
# in test
inject.clear_and_configure(lambda binder: binder
.bind(DatabaseInterface, InMemoryDatabse()))
Run Code Online (Sandbox Code Playgroud)
我的问题是:
依赖注入技术有几个主要目标,包括(但不限于):
另一件要记住的事情是我们通常应该依赖抽象,而不是实现。我看到很多人使用 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)
Run Code Online (Sandbox Code Playgroud)
看?我们甚至没有构造函数。我们以声明方式定义依赖项并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)
Run Code Online (Sandbox Code Playgroud)
并使用它:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Run Code Online (Sandbox Code Playgroud)
看?现在我们的班级不知道是谁以及如何创建它们。没有装饰器,没有特殊值。
在此处阅读有关 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
Run Code Online (Sandbox Code Playgroud)
这种模式的唯一问题是_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
Run Code Online (Sandbox Code Playgroud)
例如,RequiresContext有特殊的.map方法将其自身与纯函数组合。就是这样。因此,您只有简单的函数和具有简单 API 的组合助手。没有魔法,没有额外的复杂性。作为奖励,所有内容都正确键入并与mypy.
在这里阅读有关此方法的更多信息:
小智 0
最初的示例非常接近“正确的”干净/十六进制。缺少的是组合根的想法,并且您可以在没有任何注入器框架的情况下执行 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))
Run Code Online (Sandbox Code Playgroud)
其名称为“Pure/Vanilla/Poor Man's DI”,具体取决于您与谁交谈。抽象接口并不是绝对必要的,因为您可以依赖鸭子类型或结构类型。
是否要使用 DI 框架是一个观点和品味的问题,但是如果您选择沿着这条路走,您可以考虑其他更简单的注入替代方案,例如 punq。
https://www.cosmicpython.com/是一个很好的资源,可以深入研究这些问题。
| 归档时间: |
|
| 查看次数: |
1617 次 |
| 最近记录: |