【问题标题】:Does the prototype pattern comply with dependency injection?原型模式是否符合依赖注入?
【发布时间】:2014-10-08 06:58:01
【问题描述】:

在学习依赖注入(并获得第一次实践经验)的过程中,我想知道在考虑一个我想在不久的将来使用 DI 解决的具体项目时遇到的一个问题。

对于不同的分析,我想动态创建注入依赖项的对象,因为我需要任意数量的对象,这可能会因用户与我的程序的交互而有所不同。我考虑将这个要求实现为抽象原型模式

public interface IAnalysis
{
    SomeDataType DoSomething();
    IAnalysis CreateObject();
}

从 IAnalysis 派生的类将负责从 CreateObject() 返回该类的新对象。依赖类可以在不知道具体类型的情况下创建新对象,但只依赖于接口,因此遵循了 DI 的一个主要概念。无论如何,从 IAnalysis 派生的类必须使用 new 关键字创建新对象。我读到使用 DI 时应避免在注入器外部使用 new 创建对象,因此我不太确定这在 DI 中是否“允许”。另一方面,这对我来说似乎是一个非常明智的解决方案,因为类只会创建自己的新对象,这实际上不应该损害 DI 原则。

我想到的这个概念合理吗?我可以使用其他解决方案来实现这一目标吗?我实际上考虑过抽象工厂,但这会损害我对 DI 原则的理解。

【问题讨论】:

  • 我想知道您是否会混淆 DI 和 IoC。它们是相关的,但肯定不是一回事。 DI 要求从外部传入对象的依赖项,而不是在内部创建。没有什么可以阻止您通过其类型的构造函数创建依赖项。
  • 不,我实际上是说 DI。我打算有一个注入器类来构建我的依赖关系图。这真的有影响吗?
  • Re: “我读到在使用 DI 时应该避免构造函数”。不;应该避免的不是构造函数本身,而是直接通过new 运算符调用它们。众所周知,构造函数在 DI 中非常有用(即用于注入所需的依赖项)。
  • @stakx 我可能应该写“[...] 应该避免在注射器之外”。当然需要它们,尤其是注入依赖项。
  • @PaulKertscher:那仍然是不准确的。我想指出构造函数和new … 表达式不是一回事。 (后者使用前者创建新对象;但new …表达式不是构造函数。)您阅读的建议很可能是关于new(因为只有那时是否有意义),但您将其引用为“我读到 constructors 应该……”。

标签: c# design-patterns dependency-injection prototype-pattern


【解决方案1】:

我读到使用 DI [...] 时应避免在注入器外部使用 new 创建对象。

这只是部分正确。我将逐步向您展示new 有它的位置,使用new 来实现您的原型模式可能会很好。

让我们从显而易见的开始:如果我们需要B 类型的实例,那么它必须由某人在某处创建。假设我们有这个:

class C
{
    void Baz()
    {
        B b = new B(new A(…));
        b.Bar();
    }
}

Baz 需要B 才能完成其工作。如果我们想避免new B(…),我们能做的最好的就是从代码库中的这个特定位置删除它:

class C
{
    C(Func<B> newB) // instead of Func<B>, we could also inject a B directly
    {               // (the difference being that we would no longer control
        this.newB = newB;                        // when the B gets created)
    }

    Func<B> newB;

    void Baz()
    {
        var b = newB();
        b.Bar();
    }
}

但是传递给C 的构造函数的B 仍然必须在某个地方创建。只是现在它在别的地方。

那么我们通过避免new 获得了什么? C 不再需要了解如何准确地创建 B

但是Func&lt;B&gt; newB(即工厂方法)本身如何在不使用new 的情况下创建B?看来我们不能永远回避new

为了强调这一点,让我们继续看另一个非常相关的示例,它与您的问题更接近(在 DI 上下文中实现原型模式):抽象工厂,另一种设计模式。假设我们有一个 BFactory,其唯一职责是创建 B 类型的实例:

interface BFactory
{
    B CreateB();
}

我们可以在不使用new 的情况下实现它吗?让我们以与上述相同的方式尝试:

class RedundantBFactory : BFactory
{
    RedundantBFactory(Func<B> newB)
    {
        this.newB = newB;
    }

    Func<B> newB;

    public B CreateB()
    {
        return newB();
    }
}

这绝对没有意义!工厂存在的全部理由是它封装了有关如何创建某种类型的实例的知识。仅仅因为我们想避免在我们的工厂中使用new,我们已经将这些知识完全外化,使整个工厂完全多余(因为它只是将自己的主要责任转交给另一方,另一方必须做同样的工作)!

如果我们不希望它们完全冗余,我们可以得出结论,在抽象工厂和工厂方法中使用new(例如在BFactory甚至newB中)是合理且适当的:

class UsefulBFactory : BFactory
{
    public UsefulAFactory(Func<A> newA)
    {
        this.newA = newA;
    }

    Func<A> newA;

    public B CreateB()
    {
        return new B(newA());
    }
}

现在回到你的原型模式:原型模式本质上是关于对象克隆的。也就是说,所有实现IAnalysis 接口的类型都必须能够创建实例的克隆(副本)。就像上面的抽象工厂示例一样,接口的唯一目的是封装某种形式的对象创建。这是它首先存在的原因,因此实现此接口的类不得将该责任委托给外部方。同样,在这种情况下使用 new 是完全合理的:

class W : IAnalysis
{
    W(X x, Y y, …)
    {
        this.x = x;
        this.y = y;
        …
    }

    public IAnalysis CreateObject()
    {
        return new W(x, y, …);
    }
}

最后一句话,只是为了强调并补充我最初的主张,即避免new 在所有情况下都没有意义:请注意,无论如何所有事情都不应该使用 DI。

通常,您必须决定哪些类型应该由 DI 容器处理。这些所谓的依赖项、组件或服务通常被抽象为interfaceabstract class BaseClass,以便您以后可以用一个实现替换另一个实现。您使用new Service(…) 的唯一位置应该是在组合根中,或者(如上所示)在抽象工厂或工厂方法中(它们本身就是依赖项,将被注入到您需要在您选择的时间创建对象的位置)。如果new Service(…) 大量散布在整个代码库中,则很难将一种实现替换为另一种实现。

但是使用new 创建原始值和值类型的实例(例如stringTimeSpan 等)是完全可以的。这些类型通常不会被 DI 容器实例化。

【讨论】:

    【解决方案2】:

    我读到在使用 DI 时应该避免构造函数,因此我是 不太确定这在 DI 中是否“允许”。

    这不是它的工作原理,但构造函数是实现的一个实现细节,由于消费者只知道抽象,他们不能调用构造函数。

    但这些类型需要由某人创建。这个人是Composition Root。如果您使用pure DI,您将调用这些构造函数。如果您使用 DI 容器,该容器将代表您调用构造函数。

    记住这一点,可以使用 new 关键字创建类,但是当涉及到 injection constructors 时,您应该将此创建保留在组合根的本地。这样做的方法是在组合根内部定义IAnalysis 实现。这样,应用程序的其余部分就不需要依赖于该具体类型及其构造函数。如果您使用 DI 库,则该实现可以依赖于容器并调用它来请求新实例。

    【讨论】:

    • 我了解消费者不了解构造函数,因此无法调用它们,但恐怕我不明白这与我关于使用原型模式和调用的问题有何关系从内部实现类的构造函数。
    【解决方案3】:

    我认为依赖注入的概念是,当你向另一个类注入一些东西时。 现在,为什么要使用依赖注入,因为 OOP 概念,并且类需要使用另一个类(依赖概念)。

    现在,这里的注入概念意味着您应该向类注入一些东西。你怎么注射它?通过将其放入参数中。

    您必须在接口之外创建对象,然后将对象/类作为参数传递给接口对象。

    那么,与其尝试创建服务对象,不如传入任何需要处理的参数或对象,然后界面会为您返回结果?

    将其视为私人杀手公司的服务帮助台(接口对象)。 这个公司(接口对象)应该是私有的,他们不能给出他们的杀手名单(实现类)。

    作为客户,您将如何与这家公司进行交易(接口对象)? 您将提供您的信息,供公司使用。 然后你会在报纸上看到你想要杀死的人,作为他们工作的结果(接口对象的返回值)。

    谢谢,

    【讨论】:

    • 非常有趣的例子,但不是我的问题的合适答案。用你的例子来说,我不想从固定的杀手池中雇佣一名杀手,但我想按需创建杀手,因为我不知道我需要多少杀手(不不过感觉)。
    • mm..你很有钱..无论如何,你不能要求杀手公司提交他们的杀手名单。这是游戏规则。但是,您可以询问另一家供应商公司,这里的杀手公司创建(可能是工厂类)是谁?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-07
    • 2018-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多