【问题标题】:passing dependencies of dependencies using manual constructor injection in python在python中使用手动构造函数注入传递依赖项的依赖项
【发布时间】:2013-01-18 14:49:51
【问题描述】:

我的情况

我目前正在编写一个 Python 项目,我想用它来学习更多关于软件架构的知识。我已经阅读了一些文本并观看了一些关于依赖注入的讨论,并学会了喜欢构造函数注入如何清晰地显示对象的依赖关系。 但是,我有点挣扎如何将依赖项传递给对象。我决定不使用 DI 框架,因为:

  1. 我没有足够的 DI 知识来指定我的要求,因此无法选择框架。
  2. 我想让代码不包含更多“神奇”的东西,因为我觉得引入一个很少使用的框架会大大降低可读性。 (更多代码需要阅读,其中只使用了一小部分)。

因此,我使用自定义工厂函数来创建对象并显式传递它们的依赖项:

    # Business and Data Objects
class Foo:
    def __init__(self,bar):
        self.bar = bar
    def do_stuff(self):
        print(self.bar)

class Bar:
    def __init__(self,prefix):
        self.prefix = prefix
    def __str__(self):
        return str(self.prefix)+"Hello"
    
# Wiring up dependencies
def create_bar():
    return Bar("Bar says: ")

def create_foo():
    return Foo(create_bar())

# Starting the application
f = create_foo()
f.do_stuff()

或者,如果Foo 必须自己创建多个Bars,它会通过其构造函数获取创建者函数:

# Business and Data Objects
class Foo:
    def __init__(self,create_bar):
        self.create_bar = create_bar
    def do_stuff(self,times):
        for _ in range(times):
            bar = self.create_bar()
            print(bar)

class Bar:
    def __init__(self,greeting):
        self.greeting = greeting
    def __str__(self):
        return self.greeting
    
# Wiring up dependencies
def create_bar():
    return Bar("Hello World")

def create_foo():
    return Foo(create_bar)

# Starting the application
f = create_foo()
f.do_stuff(3)

虽然我很想听听有关代码的改进建议,但这并不是本文的真正重点。不过,我觉得这个介绍是需要了解的

我的问题

虽然上面对我来说看起来相当清晰、可读和可以理解,但当Bar 的前缀依赖项在每个Foo 对象的上下文中必须相同并因此与@ 耦合时,我遇到了一个问题987654328@ 对象生命周期。作为一个例子,考虑一个实现计数器的前缀(有关实现细节,请参见下面的代码示例)。 我有两个想法如何实现这一点,但是,它们对我来说似乎都不完美:

1) 通过 Foo 传递前缀

第一个想法是给Foo添加一个构造函数参数,让它在每个Foo实例中存储前缀。

明显的缺点是,它混淆了Foo 的职责。它控制业务逻辑并为Bar 提供依赖项之一。一旦Bar 不再需要依赖,Foo 就必须修改。对我来说似乎是不行的。由于我真的不认为这应该是一个解决方案,因此我没有在此处发布代码,而是为非常感兴趣的读者提供了 on pastebin ;)

2) 使用带状态的函数

这种方法不是将Prefix 对象放在Foo 中,而是尝试将其封装在create_foo 函数中。通过为每个Foo 对象创建一个Prefix 并使用lambda 在一个无名函数中引用它,我将细节(a.k.a there-is-a-prefix-object)远离Foo 并在我的接线内 -逻辑。当然,命名函数也可以工作(但 lambda 更短)。

# Business and Data Objects
class Foo:
    def __init__(self,create_bar):
        self.create_bar = create_bar
    def do_stuff(self,times):
        for _ in range(times):
            bar = self.create_bar()
            print(bar)

class Bar:
    def __init__(self,prefix):
        self.prefix = prefix
    def __str__(self):
        return str(self.prefix)+"Hello"
        
class Prefix:
    def __init__(self,name):
        self.name = name
        self.count = 0
    def __str__(self):
        self.count +=1
        return self.name+" "+str(self.count)+": "
    
# Wiring up dependencies
def create_bar(prefix):
    return Bar(prefix)


def create_prefix(name):
    return Prefix(name)


def create_foo(name):
    prefix = create_prefix(name)
    return Foo(lambda : create_bar(prefix))

# Starting the application
f1 = create_foo("foo1")
f2 = create_foo("foo2")
f1.do_stuff(3)
f2.do_stuff(2)
f1.do_stuff(2)

这种方法对我来说似乎更有用。但是,我不确定常见的做法,因此担心不建议在函数内部使用状态。来自 java/C++ 背景,我希望一个函数依赖于它的参数、它的类成员(如果它是一个方法)或一些全局状态。因此,不使用全局状态的无参数函数每次调用时都必须返回完全相同的值。这不是这里的情况。一旦返回的对象被修改(这意味着prefix 中的counter 已增加),该函数返回一个对象,该对象的状态与第一次返回时的状态不同。 这种假设是否只是由于我在 python 方面的有限经验引起的,我是否必须改变我的心态,即不要考虑函数而是 something 可调用的?还是为函数提供状态是对 lambda 的无意误用?

3) 使用可调用类

为了克服我对有状态函数的疑虑,我可以使用可调用类,其中方法 2 的 create_foo 函数将被替换为:

class BarCreator:
    def __init__(self, prefix):
        self.prefix = prefix
    def __call__(self):
        return create_bar(self.prefix)
     
def create_foo(name):
    return Foo(BarCreator(create_prefix(name)))

虽然这对我来说似乎是一个可用的解决方案,但它太冗长了。

总结

我不确定如何处理这种情况。虽然我更喜欢 2 号,但我仍然有疑问。此外,我仍然希望任何人都想出一个更优雅的方式。

如果有任何您认为过于模糊或可能被误解的内容,请发表评论。我会在我的能力允许的范围内改进这个问题:) 所有示例都应在 python2.7 和 python3 下运行 - 如果您遇到任何问题,请在 cmets 中报告,我会尝试修复我的代码。

【问题讨论】:

  • 除非 create_prefix() 有副作用,否则选项 2 中的 create_foo() 函数已经没有副作用。这里可观察到的副作用是什么?您应该通过在可调用对象中评估它来延迟 foo 的创建,否则它很好。
  • 我认为你是对的,我使用的功能是无副作用的。我所指的事实是create_bar 参数的结果既不依赖于参数也不依赖于全局状态,而是可以通过修改其返回值来修改。我将改写我问题的相应部分。感谢您指出这一点。

标签: python dependency-injection inversion-of-control manual


【解决方案1】:

如果您想注入一个可调用对象,但不希望它具有复杂的设置——如果像在您的示例中那样,它实际上只是绑定到单个输入值——您可以尝试使用 functools.partial 来提供一个函数值对:

def factory_function(arg):
    #processing here
    return configurted_object_base_on_arg

class Consumer(object):
   def __init__(self, injection):
      self._injected = injection

   def use_injected_value():
      print self._injected()

injectable = functools.partial(factory_function, 'this is the configuration argument')
example = Consumer(injectable)
example.use_injected_value() # should return the result of your factory function and argument

顺便说一句,如果您要创建类似于选项 3 的依赖注入设置,您可能希望将有关如何进行配置的知识放入工厂类中,而不是像您在这里所做的那样内联。这样,如果您想在策略之间进行选择,您可以更换工厂。它在功能上并没有太大的不同(除非创建比这个例子更复杂并且涉及持久状态)但是如果代码看起来像这样,它会更加灵活

factory = FooBarFactory()
bar1 = factory.create_bar()
alt_factory = FooBlahFactory(extra_info)
bar2 = alt_factory.create_bar()

【讨论】:

  • 偏函数看起来确实很好吃。谢谢你给我这个提示!它似乎结合了可调用类(可以预期它具有状态)和 lambda(create_foo 的唯一代码行)的积极方面。有关完整示例,请参阅 [pastebin] (pastebin.com/r8QuqPuc)。
  • 但是,我并没有真正理解您评论后半部分的意义。你是在建议this 之类的东西吗?如果是,我还没有看到改进。你能说明一下优点吗?
  • 我的主要观点是使用工厂类(而不是工厂函数)在更复杂的情况下可能有用。工厂类可以封装有关如何配置实例的所有知识——每当您必须协调复杂的类(例如,具有特殊规则的业务对象)时,将这些知识本地化在一个地方是很好的。另外,您可以在更高级别更换工厂 claaaes:请参阅en.wikipedia.org/wiki/Abstract_factory_pattern
  • 在我看来,抽象工厂只不过是使用行为 strategy 模式作为创建模式。我想做一些事情(创建一个具有特定接口的对象)但想把它留给策略(抽象工厂的实现)来定义我如何做到这一点(即传递哪些参数,如何创建对象以及哪个它应该实例化的类)。由于函数是 Python 中的一等公民,因此(恕我直言)不需要重新实现工厂/策略模式。如果您不同意,我很乐意通过一个例子来说明您的观点。
  • 在 python 世界中,它主要是关于保留未来的灵活性——如果未来的一些实现需要有状态,你不必做重大的返工。如果您认为情况并非如此,那或多或少是一次洗礼。只需使用鸭子类型,您就可以在 python 中获得许多相同的好处——任何类都可以成为您的工厂,如果它具有“create_bar”方法,因此不需要抽象工厂模式的整个强类型方面。
猜你喜欢
  • 2021-01-09
  • 2011-02-02
  • 1970-01-01
  • 1970-01-01
  • 2019-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-17
相关资源
最近更新 更多