【问题标题】:Inversion of control and Dependency Injection控制反转和依赖注入
【发布时间】:2012-02-05 20:58:01
【问题描述】:

这里有一个非常受关注的概念:Ioc(控制反转)。 我使用这个概念已经有一段时间了。通常我会选择 DI(而不是服务定位器)方法在我的代码中实现 IoC。 以下是我对 IoC 的理解。

如果 classA 依赖于 classB,如果它在其中声明了 classB 的实例。它依赖于 B 类,出于某种原因(我稍后会谈到),不好,因此我们使用 DI 来解决这个问题。所以现在我们有一个名为 IClassB 的接口,它将在 classA 的构造函数中传递(为了说明,我在这里使用构造函数注入)。代码如下:

public class ClassA
{
    IClassB _classBObj = null;

    public ClassA(IClassB classBObj)
    {
         _classBObj = classBObj;
    }

    public void UseClassBMethod()
    {
        this._classBObj.classBMethod();
    }
}

public class ClassB:IClassB
{

    public void classBMethod()
    {
        //Some task carried out by classB.
    }
}

public interface IClassB
{
    void classBMethod();
}

下面是让它运行的代码:

class Program
{
    static void Main(string[] args)
    {
        //This code is outside of class A and hence declaring 
        //object of ClassB is not dependency
        ClassA classA=new ClassA(new ClassB);
        classA.UseClassBMethod();

    }
}

我演示了上面的示例只是为了确保我对 IoC 和 DI 的理解是否正确。如果您发现任何错误,请纠正我。 现在,当我试图找出 IoC 的原因时,我发现了两个重要原因:

  1. 可测试性:当然是一个正确的问题。假设,例如,在上面的例子中,classB 方法使用 SMPTP 服务器或一些 WCF/Web 服务,我们可能无法对其进行测试。但是,我们可以创建一个实现接口 IClassB 的测试存根类,并通过传递一个测试存根类的实例来继续测试。

  2. 具体实施方面的依赖性:这是我无法接受的。即使我用以下代码更改了classA的代码:

public class ClassA
{
   ClassB _classBObj = null;

   public ClassA()
   {
     _classBObj = new ClassB();
   }

   public void UseClassBMethod()
   {
      this._classBObj.classBMethod();
   }
}

IoC 如何帮助解决因 ClassB 对象的 classB 方法发生变化而出现的任何问题?如果方法签名有任何变化,我们也必须在接口 IClassB 的方法签名中进行更改(实际上,这增加了重构任务。)。如果方法实现发生更改,例如它占用了额外的日志记录任务,则更改将仅限于 ClassB 代码。

这个问题我可能看起来有点愚蠢,但是有人可以提出一个场景(除了单元可测试性之外)我们从这种方法中受益吗?

非常感谢您阅读本文。

【问题讨论】:

标签: design-patterns architecture dependency-injection inversion-of-control


【解决方案1】:

这里的核心概念是你program against an interface
您的代码不知道任何具体的类。

因此,您可以在不影响代码的情况下切换具体实现。

在您的情况下,您可以将 ClassA classA=new ClassA(new ClassB); 替换为 ClassA classA=new ClassA(new ClassC);,它具有 不同的行为,例如或多或少是最佳的,或者需要一些密码才能做某事等。

这个想法是,如果ClassC 遵循ClassB 也实现的接口,那么您可以更改为使用ClassC 而无需 您的代码中的任何更改

如果界面发生变化,你的代码当然会受到影响。这是你无法避免的。

但是您获得的是,您可以在不影响代码的情况下切换和更改具体实现,并且这种更改比根据您的需要正确定义的接口更频繁地需要和发生

【讨论】:

  • +1 用于解释 DI 的概念。只想补充一点,除了接口之外,您还可以针对(抽象)基类进行编程。这就是为什么经常使用术语抽象而不仅仅是接口
  • 谢谢。 @用户384706。我相信“ClassA classA=new ClassA(new ClassB); with ClassA classA=new ClassA(new ClassC);”就是我想要的。你是对的。但是,您是否认为,在方法名称和签名保持不变的情况下,需要进行巨大的重构任务?
  • @James:通常在设计阶段,所请求功能的抽象高级视图是很好理解的。所以programming against an interface的概念所基于的接口和抽象基类,是不太可能的更改,以便您的代码需要重构。各种实现细节和需求通常在早期阶段是未知的,并且可能会发生变化。例如。可能会出现一个新的用例,并且一个方法现在必须请求用户身份验证。因此,具体类中的代码更改(预计会更频繁地更改)不会影响您的代码
【解决方案2】:

我认为在某些情况下您不想将实例注入到构造函数中。我经常注入惰性对象或工厂委托。我将尝试快速解释您为什么要这样做。

在某些现实世界的场景中,您最终可能会向构造函数中注入大量实例,有时取决于应用程序逻辑,在对象的生命周期中实际使用的实例中只有少数。这意味着有时您会初始化一个依赖项,即使它没有被使用。

在 .NET 4 中,您可以使用 Lazy 类,它允许您仅在第一次使用依赖类时实例化它。当实例化依赖项需要很长时间或实例需要大量内存时,这很酷。

public class ClassA
{
    private readonly Lazy<IClassB> _lazyClassBObj;

    public ClassA(Lazy<IClassB> lazyClassBObj)
    {
      _lazyClassBObj = lazyClassBObj;        
    }

    public void UseClassBMethod()
    {
        _lazyClassBObj.Value.classBMethod();
    }
}

class Program
{
    static void Main(string[] args)
    {
        ClassA classA = new ClassA (new Lazy<IClassB>(() => new ClassB));
        ...

    }
}

另一个技巧是将工厂委托注入构造函数而不是实例。它与 Lazy 解决方案非常相似,但有一些优点。如果您的类必须能够创建任意数量的依赖类的新实例(希望在循环中创建实例或类似的东西),注入工厂委托很酷。在此示例中,ClassA 将引用一个方法,该方法可以创建一个实现 IClassB 的对象。为了使事情更有趣,实现 IClassB 的 ClassB 也具有 IClassC 依赖项。

public class ClassA
{
    private readonly Lazy<IClassC> _lazyClassBObj;
    private readonly Func<IClassC, IClassB> _classBObjFactory;

    public ClassA(Func<IClassC, IClassB> classBObjFactory, Lazy<IClassC> lazyClassBObj)
    {
      _classBObjFactory = classBObjFactory;
      _lazyClassBObj = lazyClassBObj;        
    }

    public void UseClassBMethod()
    {
        var classC = _lazyClassBObj.Value;

        var classB1 = _classBObjFactory(classC); // instance 1
        var classB2 = _classBObjFactory(classC); // instance 2
        var classB3 = _classBObjFactory(classC); // instance 3
    }
}

这种方法的主要好处是通过不初始化您可能不使用的对象来减少内存需求。工厂委托注入的好处是您可以隐藏依赖项的初始化逻辑,并且您可以控制向调用者公开哪些工厂参数。 ClassA 不需要知道如何组装它的所有依赖项,它只需要知道它知道并且可以控制的参数。它允许您更轻松地替换依赖项。

我希望它有意义 :) 只是想演示如何使用 C# 函数式样式从模式中获得更多收益。

【讨论】:

  • +1 用于解释这些概念。谢谢你。我绝对期待使用它们。
  • 这个答案没有得到足够的爱。由于帖子中解释的所有原因,我也偏向于注入工厂方法委托。如果您查看 Jon Skeet 的“miscutil”,我相信他也在一些场景中使用了这种方法。
猜你喜欢
  • 1970-01-01
  • 2010-10-31
  • 1970-01-01
  • 2011-03-14
  • 1970-01-01
  • 2015-01-09
  • 2015-07-30
  • 1970-01-01
相关资源
最近更新 更多