我正在尝试将依赖项注入Django视图(控制器?)。这是一些背景。
通常,urls.py
文件是处理路由的文件。通常是这样的:
urlpatterns = [
path("", views.get_all_posts, name="get_all_posts"),
path("<int:post_id>", views.get_post, name="get_post"),
path("create", views.create_post, name="create_post"),
]
这个问题是,例如,一旦您进入create_post
,您可能就依赖于创建帖子的服务:
# views.py
...
def create_post(self):
svc = PostCreationService()
svc.create_post()
这种模式很难测试。虽然我知道python测试库提供了可模拟此类事情的工具,但我还是希望将依赖项注入视图中。这就是我的想法。
具有静态方法export(deps)
的Controller类,该方法接收依赖项列表并返回url模式对象列表:
class ApiController(object):
@staticmethod
def export(**deps):
ctrl = ApiController(**deps)
return [
path("", ctrl.get_all_posts, name="get_all_posts"),
path("<int:post_id>", ctrl.get_post, name="get_post"),
path("create", ctrl.create_post, name="create_post"),
]
def __init__(self, **deps):
self.deps = deps
def get_all_posts():
pass
...
这看起来很简陋,但我不知道有其他方法可以做我想做的事情。控制器需要返回url模式列表,并且还需要接受依赖项列表。使用以上技术,我可以在urls.py
中进行此操作:
urlpatterns = ApiController.export(foo_service=(lambda x: x))
我现在可以在foo_service
的任何一种方法中自由使用ApiController
。
注意:
一种替代方法是让构造函数返回url列表,但我认为这并不是对此的巨大改进。实际上,这让我感到更加困惑,因为类构造函数将返回列表而不是类的实例。
注释2:
我知道python有模拟类成员的模拟工具。请不要建议使用它们。我想使用DI作为控制和管理依赖项的方式。
任何关于最佳方法的想法是什么?
答案 0 :(得分:6)
您可以看看https://github.com/ets-labs/python-dependency-injector,但这是一个很大的设置。
您还可以创建诸如服务工厂之类的小东西
# services.py
class ServiceFactory:
def __init__(self):
self.__services = {}
def register(self, name, service_class):
# Maybe add some validation
self.__services[name] = service_class
def create(self, name, *args, **kwargs):
# Maybe add some error handling or fallbacks
return self.__services[name](*args, **kwargs)
factory = ServiceFactory()
# In your settings.py for example
from services import factory
factory.register('post_creation', PostCreationService)
# Or maybe in apps.ready do auto_load that will loop all apps and get config from services.py
# In your views.py
from services import factory
def create_post(self):
svc = factory.create('post_creation')
svc.create_post()
# In your tests.py
from services import factory
def setUp(self):
factory.register('post_creation', FakePostCreationService)
答案 1 :(得分:4)
考虑使用装饰器进行注入:
from functools import wraps
class ServiceInjector:
def __init__(self):
self.deps = {}
def register(self, name=None):
name = name
def decorator(thing):
"""
thing here can be class or function or anything really
"""
if not name:
if not hasattr(thing, "__name__"):
raise Exception("no name")
thing_name = thing.__name__
else:
thing_name = name
self.deps[thing_name] = thing
return thing
return decorator
def inject(self, func):
@wraps(func)
def decorated(*args, **kwargs):
new_args = args + (self.deps, )
return func(*new_args, **kwargs)
return decorated
# usage:
si = ServiceInjector()
# use func.__name__, registering func
@si.register()
def foo(*args):
return sum(args)
# we can rename what it's been registered as, here, the class is registered
# with name `UpperCase` instead of the class name `UpperCaseRepresentation`
@si.register(name="UpperCase")
class UpperCaseRepresentation:
def __init__(self, value):
self.value = value
def __str__(self):
return self.value.upper()
#register float
si.register(name="PI")(3.141592653)
# inject into functions
@si.inject
def bar(a, b, c, _deps): # the last one in *args would be receiving the dependencies
UpperCase, PI, foo = _deps['UpperCase'], _deps['PI'], _deps['foo']
print(UpperCase('abc')) # ABC
print(PI) # 3.141592653
print(foo(a, b, c, 4, 5)) # = 15
bar(1, 2, 3)
# inject into class methods
class Foo:
@si.inject
def my_method(self, a, b, _deps, kwarg1=30):
return _deps['foo'](a, b, kwarg1)
print(Foo().my_method(1, 2, kwarg1=50)) # = 53
答案 2 :(得分:2)
这只是上面rabbit.aaron条回复的更新版本。我的想法是能够指定要注入的依赖项,而不是获得包含所有已注册依赖项的字典。
from functools import wraps
class ServiceInjector:
deps = {}
def register(self, name=None):
name = name
def decorator(thing):
"""
thing here can be class or function or anything really
"""
if not name:
if not hasattr(thing, '__name__'):
raise Exception('no name')
thing_name = thing.__name__
else:
thing_name = name
self.__class__.deps[thing_name] = thing
return thing
return decorator
class inject:
def __init__(self, *args):
self.selected_deps = args
def __call__(self, func):
@wraps(func)
def decorated(*args, **kwargs):
selected_deps = {k: v for k, v in ServiceInjector.deps.items() if k in self.selected_deps}
new_kwargs = {**kwargs, **selected_deps}
return func(*args, **new_kwargs)
return decorated
si = ServiceInjector()
# use func.__name__, registering func
@si.register()
def foo(*args):
return sum(args)
@si.register(name='uppercase')
class UpperCaseRepresentation:
def __init__(self, value):
self.value = value
def __str__(self):
return self.value.upper()
si.register(name="PI")(3.141592653)
@si.inject('foo', 'PI', 'uppercase')
def bar(a, b, c, uppercase: UpperCaseRepresentation, **kwargs):
"""
You can specify dependencies as keyword arguments and add typehint annotation.
"""
UpperCase, foo = kwargs['UpperCase'], kwargs['foo']
print(uppercase('abc')) # ABC
print(PI) # 3.141592653
print(foo(a, b, c, 4, 5)) # = 15
bar(1, 2, 3)
class Bar:
@si.inject('foo')
def my_method(self, a, b, foo, kwarg1=30):
return foo(a, b, kwarg1)
print(Bar().my_method(1, 2, kwarg1=50)) # = 53
答案 3 :(得分:0)
您可以走烧瓶路线,并导出一个类实例,该类实例具有在首次访问时初始化和缓存服务的属性。例如:
def default_factory():
pass
# service.py
class ServiceProvider:
def __init__(self, create_instance=default_factory):
self.create_instance = create_instance
_instance = None
@property
def service(self):
if self._instance:
return self._instance
self._instance = self.create_instance()
return self._instance
service_provider = ServiceProvider()
from .service import service_provider
# views.py
def view(request):
service_provider.service.do_stuff()
# etc.
其优点是易于模拟并且没有任何魔法。
答案 4 :(得分:0)
我能想到的最无聊的解决方案是使用类变量:
# Module services.post_service
def default_create_post():
return "foo"
class Provider:
create_post = default_create_post
然后就可以在视图或其他地方正常导入和使用了:
from services import post_service
post_service.Provider.create_post()
# Should return "foo"
并且在测试时它可以在被调用之前换掉:
from django.test import TestCase
from services import post_service
from unittest.mock import patch
class MyTestCase(TestCase):
@patch('services.post_service.default_create_post')
def test_some_view(self, mock_create_post):
mock_create_post.return_value = "bar"
post_service.Provider.create_post = mock_create_post
# Now when calling post_service.Provider.create_post it should just return "bar"