这是在Django中进行依赖项注入的正确方法吗?

时间:2019-06-11 01:25:55

标签: python django

我正在尝试将依赖项注入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作为控制和管理依赖项的方式。

任何关于最佳方法的想法是什么?

5 个答案:

答案 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"