控制反转和依赖注入

4
这里是一个非常热门的概念: Ioc (控制反转)。我已经使用这个概念有一段时间了。通常我采用 DI(而不是服务定位器)方法在我的代码中实现 IoC。以下是我对 IoC 的理解。
如果ClassA依赖于ClassB,如果它在内部声明了ClassB的实例,那么它就依赖于ClassB,出于某种原因(稍后会讲到),这并不好,因此我们使用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 / Webservice,我们可能无法对其进行测试。但是,我们可以创建一个实现接口IClassB的测试存根类,并通过传递测试存根类的实例来继续进行测试。

  2. 以具体实现为依赖:这是我无法理解的地方。即使我使用以下代码更改classA的代码:

public class ClassA
{
   ClassB _classBObj = null;

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

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

IoC如何帮助解决由于ClassB方法的更改而导致的任何问题?如果方法签名发生变更,我们还必须在接口IClassB的方法签名中进行修改(这实际上增加了重构任务)。如果方法实现发生更改,例如它需要执行附加的日志记录任务,那么更改将仅限于ClassB代码。
除了单元测试可能会受益之外,有人可以提供一个情景吗?
非常感谢您阅读此内容。

2个回答

6
这里的核心概念是,你要针对一个接口进行编程。你的代码不知道任何具体类。
因此,你可以在不影响你的代码的情况下切换具体实现。例如,你可以将"ClassA classA = new ClassA(new ClassB)"替换为 "ClassA classA = new ClassA(new ClassC)",它有不同的行为,比如更优化或需要一些密码才能执行某些操作等。
这个想法是,如果"ClassC"遵循了"ClassB"也实现的接口,那么你可以更改使用"ClassC"而不需要更改你的代码。
如果接口发生变化,当然会影响你的代码。这是无法避免的。
但是,你所获得的是,你可以在不影响你的代码的情况下切换和更改具体实现,而这种变化比按照你的需求正确定义接口更频繁地发生和出现。

+1 是为了解释 DI 的概念。还想补充一点,除了接口之外,您还可以针对(抽象)基类进行编程。这就是为什么经常使用“抽象”这个术语而不仅仅是“接口”的原因。 - Sebastian Weber
谢谢。@user384706。我相信“ClassA classA=new ClassA(new ClassB); with ClassA classA=new ClassA(new ClassC);”就是我想要的。你说得对。但是,如果方法名称和签名保持不变,你认为需要进行大量的重构任务吗? - James
@詹姆斯:通常在设计阶段,对所需功能的抽象高级视图已经充分理解。因此,接口和抽象基类是“根据接口编程”概念基础的,很可能不会改变,因此您的代码将需要进行重构。各种实现细节和需求通常在早期阶段是未知的,并且可能会改变。例如,可能会出现新的用例,现在必须请求用户认证。因此,您的具体类中的代码更改(预计更经常更改)不会影响您的代码。 - Cratylus

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));
        ...

    }
}

另一个技巧是将工厂委托注入到构造函数中,而不是注入实例。这与懒加载解决方案非常相似,但具有一些优点。如果您的类必须能够创建任意数量的依赖类的新实例(想在循环或类似的东西中创建实例),那么注入工厂委托是很酷的。在这个例子中,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# 函数式风格获得更多内容。

感谢您的翻译!以下是程序相关内容的翻译:谢谢。我肯定期待使用它们。 - James
这个答案没有得到足够的关注。我也倾向于为所有在帖子中解释的原因注入工厂方法委托。如果你看一下Jon Skeet的“miscutil”,我相信他也在一些场景中使用了这种方法。 - Jacobs Data Solutions

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接