为什么我需要一个IoC容器而不是简单的DI代码?[关闭]


598

我使用依赖注入(DI)已有一段时间了,注入了构造函数,属性或方法。我从来没有觉得需要使用控制反转(IoC)容器。但是,我阅读的内容越多,社区使用IoC容器的压力就越大。

我玩过.NET容器,如StructureMapNInjectUnityFunq。我仍然看不到IoC容器如何使我的代码受益/改进。

我也害怕在工作中开始使用容器,因为许多同事会看到他们不理解的代码。他们中的许多人可能不愿意学习新技术。

请说服我,我需要使用IoC容器。与工作中的其他开发人员交谈时,我将使用这些参数。


6
好问题。我在评论中回答这个问题,因为我对国际奥委会的想法非常陌生。看起来像即插即用组件和松散耦合的想法。是否需要使用其他组件来代替当前组件,是基于需求的。如果发生IMO,使用IOC就是为更改做准备代码。
shahkalpesh

3
@shahkalpesh,但我可以通过简单的DI实现松散耦合。但是,如果我使用配置文件,我明白了。但是,我不确定是否要使用配置文件。配置文件非常冗长,重构困难以及在多个文件之间切换。
瓦迪姆

110
仅添加一条评论,因为似乎您正在寻找主要使用IoC的理由;但是关于您的问题还需要说些其他话。您不能将代码编写成团队中最糟糕的人的水平,也不能担心他们不想学习。您不仅有责任成为一名专业人士,而且有责任发展自己的未来,并且您的团队不应让您退缩。
凯文·谢菲尔德,

4
@Kevin,实际上我们正在使用IoC。教每个人有关IoC的想法并没有想象的那么难。
Vadim

21
我不知道主持人为什么要关闭大量被质疑和有用的问题。也许在这种情况下,Will不理解这个问题,或者他不理解人们使用stackexchange的目的是什么?如果它“不适合使用stackexchange的Q&A格式”,则可能认为您的政策是错误的,并非所有这数百个有用的问题都带有数百个赞成票和赞成票。
NickG 2012年

Answers:


441

哇,简直无法相信乔尔会这样:

var svc = new ShippingService(new ProductLocator(), 
   new PricingService(), new InventoryService(), 
   new TrackingRepository(new ConfigProvider()), 
   new Logger(new EmailLogger(new ConfigProvider())));

在此:

var svc = IoC.Resolve<IShippingService>();

许多人没有意识到您的依赖项链会嵌套,并且手动将它们连接起来很快变得笨拙。即使在工厂中,重复代码也不值得。

IoC容器可能很复杂,是的。但是对于这个简单的案例,我已经证明它非常简单。


好吧,让我们进一步证明这一点。假设您有一些要绑定到智能UI的实体或模型对象。这个智能的UI(我们将其称为Shindows Morms)希望您实现INotifyPropertyChanged,以便它可以进行更改跟踪并相应地更新UI。

“好吧,听起来并不难”,所以您开始写作。

您从此开始:

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime CustomerSince { get; set; }
    public string Status { get; set; }
}

..最后是这样的

public class UglyCustomer : INotifyPropertyChanged
{
    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            string oldValue = _firstName;
            _firstName = value;
            if(oldValue != value)
                OnPropertyChanged("FirstName");
        }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set
        {
            string oldValue = _lastName;
            _lastName = value;
            if(oldValue != value)
                OnPropertyChanged("LastName");
        }
    }

    private DateTime _customerSince;
    public DateTime CustomerSince
    {
        get { return _customerSince; }
        set
        {
            DateTime oldValue = _customerSince;
            _customerSince = value;
            if(oldValue != value)
                OnPropertyChanged("CustomerSince");
        }
    }

    private string _status;
    public string Status
    {
        get { return _status; }
        set
        {
            string oldValue = _status;
            _status = value;
            if(oldValue != value)
                OnPropertyChanged("Status");
        }
    }

    protected virtual void OnPropertyChanged(string property)
    {
        var propertyChanged = PropertyChanged;

        if(propertyChanged != null)
            propertyChanged(this, new PropertyChangedEventArgs(property));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

那真是令人作呕的管道代码,我坚持认为,如果您正在手工编写这样的代码,那么您就是在从客户那里偷钱。有更好,更聪明的工作方式。

有没有听说过这个词,更聪明地工作,而不是更努力?

想象一下您的团队中一些聪明的人冒出来说:“这是一种更简单的方法”

如果您将属性虚拟化(冷静下来,没什么大不了的),那么我们可以自动编织该属性行为。(这被称为AOP,但是不用担心名称,专注于它会为您做什么)

根据您使用的IoC工具,您可以执行以下操作:

var bindingFriendlyInstance = IoC.Resolve<Customer>(new NotifyPropertyChangedWrapper());

of! 现在,将在有问题的对象的每个虚拟属性设置器上为您自动生成所有该手册INotifyPropertyChanged BS。

这是魔法吗? 是的!如果您可以相信该代码可以完成其工作,那么您可以安全地跳过所有包装mumbo-jumbo的属性。您有需要解决的业务问题。

使用IoC工具进行AOP的其他一些有趣用途:

  • 声明式和嵌套数据库事务
  • 声明性和嵌套工作单元
  • 记录中
  • 前后条件(按合同设计)

28
糟糕,您忘记了将所有属性虚拟化,但是这没有用。您是否还需要花费时间调试从客户端窃取的信息?(如果您不使用
Thom,

17
您使用INotifyPropertyChanged包装器牺牲了很多清晰度。如果您花了几分钟多的时间来编写该管道,则说明您做错了事。说到代码的冗长,少即不是多!
2009年

39
@overstood-我完全不同意。首先,在此示例中,清晰度在哪里重要?最接近消费者。消费者明确要求INotifyPropertyChanged。仅仅因为我们可以使用微代码或宏代码生成创建这种类型的代码,但这并不意味着编写它是一个好主意。INotifyPropertyChanged残酷的做法只是死记硬背,平凡无奇,难以置信,实际上会损害所讨论类的可读性。
本·谢尔曼

31
标记为下降是因为您将依赖性注入模式和服务定位器模式混淆了。应该使用前者代替后者。例如,对象(或实际上是整个系统)永远不要调用Resolve(...)来获取IShippingService的实例。需要IShippingService的对象应该在构造它们时收到对它们应该使用的实例的引用,并且更高的层负责将两个对象连接在一起。
纳特

30
无论这是什么(IoC,DynamicProxy,AOP还是黑魔法),有人可以将我链接到框架中的具体文章/特定文档,以提供实现此目的的更多详细信息吗?
fostandy,2010年

138

我和你在一起,瓦迪姆 IoC容器采用了一个简单,优雅且有用的概念,并使其成为您必须学习200天的手册才能使用两天的东西。

我个人对IoC社区如何接受Martin Fowler撰写的精美优美的文章感到困惑,并将其变成一堆通常包含200-300页手册的复杂框架。

我尽量不要有判断力(HAHA!),但是我认为使用IoC容器的人(A)非常聪明,并且(B)对那些不像他们那么聪明的人缺乏同理心。一切对他们来说都是很有意义的,因此他们很难理解许多普通程序员会发现这些概念令人困惑。这是知识诅咒。了解IoC容器的人很难相信有些人不了解它。

使用IoC容器最有价值的好处是,您可以在一个地方放置一个配置开关,以便在测试模式和生产模式之间进行切换。例如,假设您有两个版本的数据库访问类:一个版本在开发过程中经常使用日志记录并进行了大量验证,而另一个版本却没有记录或验证,而这对于生产而言却是惊人的快。能够在一处在它们之间切换是很高兴的。另一方面,这是一个相当琐碎的问题,可以轻松地以更简单的方式处理,而无需IoC容器的复杂性。

我相信,如果您使用IoC容器,坦率地说,您的代码将变得更难阅读。您必须查看的地方数目才能弄清楚代码要执行的操作至少增加了一个。在天堂的某个地方,天使在呼喊。


81
我认为您对乔尔的看法有些困惑。首先是IoC和容器,然后是Martin的论文,反之亦然。
格伦街区

55
避开大多数容器,可以让您快速上手。使用autofac,结构图,统一,ninject等,您应该能够使容器在大约5分钟内工作。是的,它们具有高级功能,但您不需要这些就可以使用。
格伦街区

58
乔尔(Joel)我曾经和您一样思考。然后,我从字面上认真地花了五分钟来弄清楚如何运行最基本的设置,并对80/20规则的执行感到惊讶。它使代码更简洁,尤其是在简单情况下。
贾斯汀·波佐尼尔

55
我已经进行了不到两年的编程,并且阅读了Ninject Wiki并获得了它。从那时起,我一直在很多地方使用DI。你看见了吗?不到两年,在Wiki上阅读了几页。我不是巫师或其他任何人。我不认为自己是天才,但我很容易理解。我无法想象真正了解OO的人无法掌握它。另外,您指的是200-300页的手册?我没看过 我使用的所有IoC容器都非常简短。
JR加西亚

35
+1写得很好,经过深思熟虑。我认为很多人没有意识到的一件事是,添加框架或类似内容以“神奇地”自动处理某些事情会增加系统的复杂性和局限性。有时这是值得的,但通常会忽略某些东西,并在代码中释放熵,以尝试解决由限制引起的问题。它可以节省前期的时间,但是人们必须意识到,解决少数晦涩的问题可能要花100倍的成本,只有少数人真正有足够的知识来解决。
kemiller2002

37

大概没有人强迫您使用DI容器框架。您已经在使用DI解耦类并提高可测试性,因此您将获得许多好处。简而言之,您倾向于简单,这通常是一件好事。

如果您的系统达到了使手工DI变得繁琐的复杂程度(即增加了维护),则应将其与DI容器框架的团队学习曲线进行权衡。

如果您需要对依赖项生存期管理进行更多控制(也就是说,如果您需要实现Singleton模式),请查看DI容器。

如果使用DI容器,请仅使用所需的功能。如果足够,请跳过XML配置文件并用代码对其进行配置。坚持构造函数注入。Unity或StructureMap的基础知识可以精简为几页。

Mark Seemann撰写了一篇很棒的博客文章:何时使用DI容器


很好的回应。我唯一意见不同的是,手动注射EVER是否可以被认为不那么繁琐或繁琐。
wekempf

+1。最好的答案之一。也感谢您的博客文章链接。
stakx-

1
+1为平衡视点。IoC容器并不总是好事,也不总是坏事。如果您看不到需求,请不要使用它。如果确实需要,只要知道该工具就可以了。
菲尔

32

在我看来,IoC的第一大好处是能够集中化依赖项的配置。

如果您当前正在使用依赖注入,则代码可能如下所示

public class CustomerPresenter
{
  public CustomerPresenter() : this(new CustomerView(), new CustomerService())
  {}

  public CustomerPresenter(ICustomerView view, ICustomerService service)
  {
    // init view/service fields
  }
  // readonly view/service fields
}

如果您使用静态的IoC类,而不是IMHO更加混乱的配置文件,则可能会有以下内容:

public class CustomerPresenter
{
  public CustomerPresenter() : this(IoC.Resolve<ICustomerView>(), IoC.Resolve<ICustomerService>())
  {}

  public CustomerPresenter(ICustomerView view, ICustomerService service)
  {
    // init view/service fields
  }
  // readonly view/service fields
}

然后,您的静态IoC类将如下所示,我在这里使用Unity。

public static IoC
{
   private static readonly IUnityContainer _container;
   static IoC()
   {
     InitializeIoC();
   }

   static void InitializeIoC()
   {
      _container = new UnityContainer();
      _container.RegisterType<ICustomerView, CustomerView>();
      _container.RegisterType<ICustomerService, CustomerService>();
      // all other RegisterTypes and RegisterInstances can go here in one file.
      // one place to change dependencies is good.
   }
}

11
Ben,您所谈论的实际上是服务位置,而不是依赖注入-有所不同。我总是更喜欢第一个。stevenharman.net/blog/archive/2009/09/25/…–
stevenharman

23
而不是在默认构造函数中执行IoC.Resolve <T>,您应该利用IOC容器为您构造对象的能力。摆脱那个空的构造函数,让IOC容器为您构建对象。var presenter = IoC.Resolve <CustomerPresenter>(); 瞧!所有依赖项已经连接起来。
Ben Scheirman

12
这种配置集中化的不利方面是知识的去上下文化。乔尔指出这个问题时说:“坦白说,代码变得更难阅读”。
Scott Bellware 09年

6
@sbellware,什么知识?被视为主要依赖项的接口可以作为契约,并且应该足以传达其目的和行为,不是吗?我想您可能是指从透支合同的执行过程中获得的知识,在这种情况下,您需要跟踪适当的配置?我依靠一台计算机(VS,IntelliJ等中的R#)来完成这些任务,因为它们非常擅长导航图的节点和边缘。我为我当前关注的工作地点保留了我的大脑。
stevenharman

7
史蒂夫,我知道.NET文本,并且走了很多年。我对其他模式和形式也非常熟悉,并且我内在地了解到存在真正的权衡。我知道如何以及何时进行这些权衡,但是我的工作生活中的多样性至少使我切实理解了乔尔的观点。告诉我一些我不知道的知识,而不是向我讲授比大多数.NET单一文化专家使用时间更长的工具和技巧。我将大脑堆放在各种批判性思考上,而不是alt.net停滞和正统思维。
Scott Bellware 09年

32

IoC容器也非常适合加载深层嵌套的类依赖项。例如,如果您使用Depedency Injection具有以下代码。

public void GetPresenter()
{
    var presenter = new CustomerPresenter(new CustomerService(new CustomerRepository(new DB())));
}

class CustomerPresenter
{
    private readonly ICustomerService service;
    public CustomerPresenter(ICustomerService service)
    {
        this.service = service;
    }
}

class CustomerService
{
    private readonly IRespository<Customer> repository;
    public CustomerService(IRespository<Customer> repository)
    {
        this.repository = repository;
    }
}

class CustomerRepository : IRespository<Customer>
{
    private readonly DB db;
    public CustomerRepository(DB db)
    {
        this.db = db;
    }
}

class DB { }

如果您已将所有这些依赖项加载到IoC容器中,则可以解析CustomerService,所有子依赖项都将自动得到解析。

例如:

public static IoC
{
   private IUnityContainer _container;
   static IoC()
   {
       InitializeIoC();
   }

   static void InitializeIoC()
   {
      _container = new UnityContainer();
      _container.RegisterType<ICustomerService, CustomerService>();
      _container.RegisterType<IRepository<Customer>, CustomerRepository>();
   }

   static T Resolve<T>()
   {
      return _container.Resolve<T>();
   }
}

public void GetPresenter()
{
   var presenter = IoC.Resolve<CustomerPresenter>();
   // presenter is loaded and all of its nested child dependencies 
   // are automatically injected
   // -
   // Also, note that only the Interfaces need to be registered
   // the concrete types like DB and CustomerPresenter will automatically 
   // resolve.
}

但是使用服务定位器反模式调用IoC类。应该使用构造函数DI而不是调用静态T Resolve <T>()来传递Should_container吗?
Michael Freidgeim

@bendewey您的示例暗示它会自动将参数传递给新的CustomerService和新的CustomerRepository的构造函数。这是这样工作的吗?如果还具有其他附加参数怎么办?
Brain2000 2013年

28

我是声明式编程的狂热者(看看我回答了多少个SQL问题),但是我看过的IoC容器似乎太不可思议了。

...或者也许IoC容器的开发人员无法编写清晰的文档。

...或者两者都在某种程度上是正确的。

我认为IoC容器的概念并不坏。但是,实现必须既强大又灵活,既要在各种应用程序中有用,又要简单易懂。

大概是六分之一,六分之一。实际的应用程序(而不是玩具或演示程序)注定是复杂的,需要考虑很多特殊情况和规则例外。要么将这种复杂性封装在命令性代码中,要么将其封装在声明性代码中。但是您必须在某个地方代表它。


5
我喜欢您对不可避免的复杂性的观察。基于容器的布线和基于手动的依赖关系布线之间的区别在于,使用容器,您可以单独关注每个组件,从而以相当线性的方式管理不断增加的复杂性。具有手动依赖性接线的系统在发展方面更具挑战性,因为配置一个组件的行为很难与其他所有配置分开。
Nicholas Blumhardt

我喜欢AspectJ。它不是Java,但也不是XML。它有点IS Java中,这感觉和看起来像Java的是它的一个扩展。我宁愿将自己的方面放在POJO和业务逻辑中。我几乎也想保留自己的IoC。恕我直言,连接工具时XML太多了。如果我必须拥有配置文件,则让它们成为BeanShell脚本。/ Java,请在此处应用.Net工作->。
克里斯·K

2
我当然理解您的回答,但是我认为问题的很大一部分是,已经为许多已经非常熟悉IoC框架的人(以及确实还有许多其他框架,也针对其他事物)进行了描述和记录。必要的概念和术语。我正在学习很多这样的东西,它们只是继承了另一个程序员的代码。我在努力理解为什么“对象图”非常小且没有真正的代码测试覆盖范围时,为什么代码使用IoC。作为SQL程序员,您可能会更好地将IoC与ORM结合使用。
肯尼·埃维特

1
@KennyEvitt,好点,如果一个IoC框架有一个简单的应用程序,甚至不费心去进行良好的测试覆盖,那么使用IoC框架似乎还为时过早。
Bill Karwin

27

使用容器主要是关于从命令式/脚本式初始化和配置样式更改为声明样式。这可能会有一些不同的有益效果:

  • 减少毛团主程序的启动例程。
  • 启用相当深的部署时重新配置功能。
  • 使依赖项可注入样式成为新工作阻力最小的路径。

当然,可能会有困难:

  • 需要复杂的启动/关闭/生命周期管理的代码可能不容易适应容器。
  • 您可能必须导航任何个人,流程和团队文化问题-但这就是为什么您问...
  • 一些工具包本身正在迅速变得重量级,这鼓励了许多DI容器开始作为一种强烈反对而产生的那种深度依赖。

23

在我看来,您已经建立了自己的IoC容器(使用Martin Fowler所描述的各种模式),并在问为什么别人的实现比您的更好。

因此,您有一堆可以使用的代码。并且想知道为什么要用其他人的实现代替它。

考虑第三方IoC容器的优点

  • 您可以免费修复错误
  • 库设计可能比您的设计更好
  • 人们可能已经熟悉特定的图书馆
  • 该库可能比您的库快
  • 它可能具有您希望实现的某些功能,但是却没有时间去使用(您有服务定位器吗?)

缺点

  • 您免费引入了错误:)
  • 库设计可能比您的差
  • 您必须学习一个新的API
  • 您永远不会使用的功能太多
  • 通常,调试您未编写的代码更加困难
  • 从以前的IoC容器迁移可能很乏味

因此,权衡利弊并做出决定。


我同意您的意见,Sam-DI是IoC的一种,因此,如果原始海报正在执行DI,则他们已经在进行IoC,因此,真正的问题是为什么要自己动手制作。
Jamie Love

3
我不同意。IOC是一种进行DI的方法。IOC容器通过使用动态运行时反射来控制类型实例化来满足和注入依赖关系,从而实现DI。OP建议他在做静态DI,并在思考为什么他需要权衡编译时检查的好处,以换取通常与IOC动态运行时反射相关的好处。
2013年

17

我认为,使用IoC可以获得IoC的大部分价值。由于您已经在执行此操作,因此其余的好处是递增的。

您获得的价值将取决于您正在处理的应用程序的类型:

  • 对于多租户,IoC容器可以处理一些基础架构代码,以加载不同的客户端资源。当您需要特定于客户端的组件时,请使用自定义选择器来处理逻辑,而不必担心客户端代码中的逻辑。您当然可以自己构建它,但这是IoC如何提供帮助的示例

  • IoC具有许多扩展点,可用于从配置中加载组件。这是很常见的事情,但是工具是由容器提供的。

  • 如果您想将AOP用于某些跨领域的问题,则IoC提供了挂钩来拦截方法调用。临时在项目上很少这样做,但IoC使其更容易。

我之前已经写过这样的功能,但是如果现在需要这些功能中的任何一个,我宁愿使用一个预先构建并经过测试的工具,如果它适合我​​的体系结构。

正如其他人所提到的,您还可以集中配置要使用的类。尽管这可能是一件好事,但它会导致误导和复杂化。大多数应用程序的核心组件并没有太多替换,因此要进行权衡有点困难。

我使用IoC容器并欣赏其功能,但不得不承认,我已经权衡利弊:我的代码在类级别变得更清晰,而在应用程序级别则变得不太清晰(即可视化控制流)。


16

我是一名正在康复的IOC上瘾者。这些天来,我发现很难证明使用IOC进行DI是合理的。IOC容器会牺牲编译时间检查,并且据此回报会为您提供“轻松”的设置,复杂的生命周期管理以及在运行时动态发现依赖项。我发现在大多数情况下,失去编译时间检查和运行时魔术/异常的损失是不值得的。在大型企业应用程序中,它们可能使后续操作变得非常困难。

我不赞成集中化参数,因为您可以通过为应用程序使用抽象工厂,并认真地将对象创建推迟到抽象工厂(即执行适当的DI),来非常轻松地集中静态设置。

为什么不使用静态无魔法的DI这样呢:

interface IServiceA { }
interface IServiceB { }
class ServiceA : IServiceA { }
class ServiceB : IServiceB { }

class StubServiceA : IServiceA { }
class StubServiceB : IServiceB { }

interface IRoot { IMiddle Middle { get; set; } }
interface IMiddle { ILeaf Leaf { get; set; } }
interface ILeaf { }

class Root : IRoot
{
    public IMiddle Middle { get; set; }

    public Root(IMiddle middle)
    {
        Middle = middle;
    }

}

class Middle : IMiddle
{
    public ILeaf Leaf { get; set; }

    public Middle(ILeaf leaf)
    {
        Leaf = leaf;
    }
}

class Leaf : ILeaf
{
    IServiceA ServiceA { get; set; }
    IServiceB ServiceB { get; set; }

    public Leaf(IServiceA serviceA, IServiceB serviceB)
    {
        ServiceA = serviceA;
        ServiceB = serviceB;
    }
}


interface IApplicationFactory
{
    IRoot CreateRoot();
}

abstract class ApplicationAbstractFactory : IApplicationFactory
{
    protected abstract IServiceA ServiceA { get; }
    protected abstract IServiceB ServiceB { get; }

    protected IMiddle CreateMiddle()
    {
        return new Middle(CreateLeaf());
    }

    protected ILeaf CreateLeaf()
    {
        return new Leaf(ServiceA,ServiceB);
    }


    public IRoot CreateRoot()
    {
        return new Root(CreateMiddle());
    }
}

class ProductionApplication : ApplicationAbstractFactory
{
    protected override IServiceA ServiceA
    {
        get { return new ServiceA(); }
    }

    protected override IServiceB ServiceB
    {
        get { return new ServiceB(); }
    }
}

class FunctionalTestsApplication : ApplicationAbstractFactory
{
    protected override IServiceA ServiceA
    {
        get { return new StubServiceA(); }
    }

    protected override IServiceB ServiceB
    {
        get { return new StubServiceB(); }
    }
}


namespace ConsoleApplication5
{
    class Program
    {
        static void Main(string[] args)
        {
            var factory = new ProductionApplication();
            var root = factory.CreateRoot();

        }
    }

    //[TestFixture]
    class FunctionalTests
    {
        //[Test]
        public void Test()
        {
            var factory = new FunctionalTestsApplication();
            var root = factory.CreateRoot();
        }
    }
}

您的容器配置是您的抽象工厂实现,您的注册是抽象成员的实现。如果需要新的单例依赖关系,只需将另一个抽象属性添加到抽象工厂。如果需要瞬时依赖,只需添加另一种方法并将其作为Func <>注入即可。

优点:

  • 所有设置和对象创建配置都是集中的。
  • 配置只是代码
  • 编译时检查使您可以轻松维护,因为您不会忘记更新注册。
  • 没有运行时反射魔术

我建议持怀疑态度的人继续进行下一个绿色项目,并诚实地问自己在什么时候需要容器。稍后将IOC容器考虑在内很容易,因为您只是将工厂实现替换为IOC容器配置模块。


在我看来,这似乎不是反对IoC的论点,而是反对可配置 IoC框架的论点。您在这里的工厂难道不可以归结为简单的硬编码 IoC吗?
2013年

我认为,当发布者提到IoC时,就意味着要使用IoC容器框架。感谢您的原始文章,这有助于说服我没有框架。因此,如果它可以在测试和生产代码中进行配置,那么“可配置”框架的优势是什么?
huggie 2013年

根据我对术语的理解,控制反转意味着在运行时查找依赖项。所以是的,您可以说工厂基本上是一个硬编码或静态容器,但它不是IOC容器。类型在编译时解析。这是反对使用可配置框架的争论,可配置框架使用字典查找在运行时解析类型。
2014年

14

对我而言,使用IoC容器(个人而言,我使用Ninject)的最大好处是消除了设置和其他类型的全局状态对象的传递。

我不为网络编程,我的程序是一个控制台应用程序,在对象树深处的许多地方,我需要访问用户指定的设置或在对象树的完全独立的分支上创建的元数据。使用IoC时,我只是简单地告诉Ninject将设置视为一个单例(因为它们总是只有一个实例),在构造函数中请求设置或字典,然后保存...当我需要它们时,它们会神奇地出现!

如果不使用IoC容器,则必须先将设置和/或元数据向下传递到2、3,...,n个对象,然后才由需要它的对象实际使用它。

正如其他人在此处详细介绍的那样,DI / IoC容器还有许多其他好处,从创建对象的想法转变为请求对象的想法可能会弯腰,但是使用DI对我和我的团队非常有用,因此也许您可以添加它到你的武器库!


2
这是一个巨大的优势。不敢相信之前没有人提到过它。无需传递或存储临时对象,可使代码更简洁。
Finglas'3

1
@qble全局对象非常有用...如果您希望代码库是紧密耦合的,不可测试的噩梦,随着时间的推移,这种噩梦将变得越来越难。祝你好运。
杰弗里·卡梅伦

2
@JeffreyCameron IoC容器全局对象。它不会删除您的全局依赖项。
qble

3
是的,但是IoC容器对应用程序的其余部分是透明的。您在启动时调用一次以获取实例,这是您最后看到的实例。使用全局对象,您的代码到处都是硬依赖性
Jeffrey Cameron

1
@qble使用工厂动态创建瞬态实例。这些工厂是单例服务。如果临时实例需要IOC容器中的某些内容,则将它们连接到工厂,并让工厂将依赖项传递到临时实例中。
杰弗里·卡梅隆

14

IoC框架非常适合您...

  • ...扔掉式安全。如果要确保所有内容都正确连接,许多(全部)IoC框架会强制您执行代码。“嘿!希望我一切都准备好了,这样我对这100个类的初始化不会在生产中失败,并抛出空指针异常!”

  • ...用全局变量乱码(IoC框架是 所有有关更改全局状态)。

  • ...编写不清晰的依赖关系的糟糕代码,很难重构,因为您永远不知道依赖什么。

IoC的问题在于,使用它们的人过去常常像这样编写代码

public class Foo {
    public Bar Apa {get;set;}
    Foo() {
        Apa = new Bar();
    }
}

这显然是有缺陷的,因为Foo和Bar之间的依赖关系是硬连接的。然后他们意识到写像这样的代码会更好

public class Foo {
    public IBar Apa {get;set;}
    Foo() {
        Apa = IoC<IBar>();
    }
}

这也是有缺陷的,但显然没有。在Haskell的类型Foo()将是IO Foo但是您实际上不希望使用IO-part,它应该是一个警告信号,表明您的设计有问题。

要摆脱它(IO部分),获得IoC框架的所有优点,而没有缺点,您可以改为使用抽象工厂。

正确的解决方案是这样的

data Foo = Foo { apa :: Bar }

或许

data Foo = forall b. (IBar b) => Foo { apa :: b }

并注入(但我不称其为注入)吧。

另外:与LINQ的发明者Erik Meijer一起观看此视频,他说DI适用于不懂数学的人(我完全不同意): http //www.youtube.com/watch?v = 8Mttjyf-8P4

与Spolsky先生不同,我不相信使用IoC框架的人非常聪明-我只是相信他们不懂数学。


9
抛弃类型安全性-除非您的示例仍然是类型安全性,否则界面就是如此。您不是在传递对象的类型,而是在传递接口的类型。而且类型安全还是不能消除null引用异常,您可以高兴地将null引用作为类型安全参数传递-根本不是类型安全问题。
2009年

因为您不知道类型是否已在IoC容器中连接(IoC <IBar>的类型不是IBar,实际上是IO IBar),所以放弃了类型安全性。是的,在Haskell示例中我无法获得空引用。
芬森2009年

类型安全性不是真正地与要连接的对象有关,至少在Java / C#中不是这样,只是有关对象的类型正确。而ConcreteClass myClass = null仍然是正确的类型。如果要强制执行不为null的代码,则代码契约将发挥作用。
2009年

2
我在某种程度上同意你的第一点,我想对第二点做一些解释,而第三点对我来说从来都不是问题。您的C#示例将IoC容器用作服务定位器,这是一个常见错误。比较C#与Haskell =苹果与橙子。
Mauricio Scheffer

2
您不会丢弃类型安全性。一些(如果不是大多数)DI容器使您可以在注册过程之后验证完整的对象图。这样做时,可以防止应用程序以错误的配置(快速失败)启动,并且我的应用程序始终具有一些调用生产配置的单元测试,以允许我在测试期间发现这些错误。我建议使用IoC容器的每个人编写一些单元测试,以确保可以解析所有顶级对象。
史蒂文

10

我发现正确实现依赖注入会迫使程序员使用各种其他编程实践,这些实践有助于提高代码的可测试性,灵活性,可维护性和可伸缩性:诸如单一职责原则,关注点分离和编码之类的实践。针对API。感觉就像我被迫编写更多的模块化,一口大小的类和方法,这使得代码更易于阅读,因为它可以按一口大小的块进行处理。

但是它也倾向于创建相当大的依赖树,通过树(特别是如果您使用约定),比手工树更容易管理。今天,我想在LINQPad中真正快速地测试某些东西,我发现创建内核并在我的模块中加载会很麻烦,而我最终手工编写了这个:

var merger = new SimpleWorkflowInstanceMerger(
    new BitFactoryLog(typeof(SimpleWorkflowInstanceMerger).FullName), 
    new WorkflowAnswerRowUtil(
        new WorkflowFieldAnswerEntMapper(),
        new ActivityFormFieldDisplayInfoEntMapper(),
        new FieldEntMapper()),
    new AnswerRowMergeInfoRepository());

回想起来,使用IoC框架会更快,因为按照惯例,这些模块几乎定义了所有这些东西。

我花了一些时间研究这个问题的答案和评论,我坚信反对使用IoC容器的人们并没有实践真正的依赖注入。我所看到的示例是通常与依赖注入混为一谈的实践。有些人抱怨“阅读”代码困难。如果正确完成,则手动使用DI时和使用IoC容器时,绝大多数代码应该相同。区别应该完全存在于应用程序中的几个“启动点”上。

换句话说,如果您不喜欢IoC容器,则可能不是按照应有的方式进行了依赖注入。

另一点:如果在任何地方使用反射,依赖注入实际上是无法手动完成的。尽管我讨厌反射对代码导航的影响,但您必须认识到确实有某些地方是无法避免的。例如,ASP.NET MVC尝试通过在每个请求上进行反射来实例化控制器。要手动进行依赖项注入,您必须使每个控制器成为“上下文根”,如下所示:

public class MyController : Controller
{
    private readonly ISimpleWorkflowInstanceMerger _simpleMerger;
    public MyController()
    {
        _simpleMerger = new SimpleWorkflowInstanceMerger(
            new BitFactoryLog(typeof(SimpleWorkflowInstanceMerger).FullName), 
            new WorkflowAnswerRowUtil(
                new WorkflowFieldAnswerEntMapper(),
                new ActivityFormFieldDisplayInfoEntMapper(),
                new FieldEntMapper()),
            new AnswerRowMergeInfoRepository())
    }
    ...
}

现在将其与允许DI框架为您做的比较:

public MyController : Controller
{
    private readonly ISimpleWorkflowInstanceMerger _simpleMerger;
    public MyController(ISimpleWorkflowInstanceMerger simpleMerger)
    {
        _simpleMerger = simpleMerger;
    }
    ...
}

使用DI框架,请注意:

  • 我可以对这堂课进行单元测试。通过创建一个模拟ISimpleWorkflowInstanceMerger,我可以测试它是否以预期的方式使用,而无需数据库连接或其他任何东西。
  • 我使用的代码少得多,而且代码更容易阅读。
  • 如果我的依赖项的依赖项之一发生了更改,则无需对控制器进行任何更改。当您考虑到多个控制器可能使用某些相同的依赖项时,这特别好。
  • 我从未从数据层中明确引用类。我的Web应用程序可以仅包含对包含ISimpleWorkflowInstanceMerger接口的项目的引用。这使我可以将应用程序分解为单独的模块,并维护真正的多层体系结构,从而使事情变得更加灵活。

一个典型的Web应用程序将具有很多控制器。随着应用程序的增长,在每个控制器中手动执行DI的所有痛苦都会加起来。如果您的应用程序只有一个上下文根,那么它永远不会尝试通过反射实例化服务,那么这并不是什么大问题。但是,除非使用某种类型的框架来管理依赖关系图,否则任何使用依赖关系注入的应用程序一旦达到一定大小,其管理成本就会变得非常昂贵。


9

每当您使用“ new”关键字时,您都在创建一个具体的类依赖关系,并且您的头脑中应该会有点警钟。孤立地测试该对象变得更加困难。解决方案是对接口进行编程并注入依赖关系,以便可以使用实现该接口的任何对象(例如,模拟)对对象进行单元测试。

问题是您必须在某个地方构造对象。工厂模式是将耦合移出POXO的一种方法(普通的“在此处插入OO语言”对象)。如果您和您的同事都在编写这样的代码,那么IoC容器就是您可以对代码库进行的下一个“增量改进”。它将所有讨厌的Factory样板代​​码从干净的对象和业务逻辑中移出。他们会爱上它。哎呀,就公司为什么喜欢它,并让所有人都热心进行公司演讲。

如果您的同事还没有做DI,那么我建议您先专注于此。广为宣传如何编写易于测试的简洁代码。干净的DI代码是困难的部分,一旦到达那儿,将对象接线逻辑从Factory类转移到IoC容器应该是相对简单的。


8

因为所有依赖项都清晰可见,所以它促进了创建松散耦合的组件,同时又易于在整个应用程序中访问和重用。


7

不需要 IoC容器。

但是,如果您严格遵循DI模式,您会发现拥有一个DI模式会删除大量的冗余代码。

无论如何,这通常是使用库/框架的最佳时间-当您了解它在做什么并且不需要库就可以做到。


6

我恰好正处于取消自制的DI代码并将其替换为IOC的过程中。我可能已经删除了200行以上的代码,并用大约10行代码替换了。是的,我不得不对如何使用容器(Winsor)进行一些学习,但是我是一名工程师,致力于使用Internet技术。 21世纪,所以我已经习惯了。我大概花了大约20分钟的时间查看方法。这很值得我度过。


3
“但是我是21世纪从事互联网技术的工程师,因此我已经习惯了”-> +1
user96403

5

随着您继续解耦类并反转依赖关系,这些类将继续保持较小状态,并且“依赖关系图”的大小将继续增长。(这还不错。)使用IoC容器的基本功能可以使所有这些对象的连接变得微不足道,但是手动进行操作可能会非常繁重。例如,如果我想创建“ Foo”的新实例但需要一个“ Bar”,该怎么办。“ Bar”需要“ A”,“ B”和“ C”。而且每个人都需要其他3样东西,等等,等等(是的,我不能拿出好假的名字:))。

使用IoC容器为您构建对象图可大大降低复杂度,并将其推送到一次性配置中。我只是说“为我创建一个'Foo'”,它指出了构建一个“ Foo”所需要的。

有些人将IoC容器用于更多基础架构,这对于高级方案是很好的,但是在那些情况下,我同意它会混淆并且使新开发人员难以阅读和调试代码。


1
为什么我们继续安抚最低的共同点,而不是努力把它们拉大?
stevenharman

4
我什么都没说。我只是说高级场景会使新开发人员更加困惑-这是事实。我没有说“所以不要这样做”。但是即使到那时(魔鬼的拥护者的时间),通常还有其他方法可以完成同一件事,从而使代码库更加明确和易于使用。仅仅因为可以隐藏隐藏基础结构工具自定义配置中的复杂性是不正确的理由,否则就不会给任何人带来麻烦。
亚伦·勒奇

4

关于Unity的Dittos。太大,就可以听到the子的吱吱作响。

当人们开始大肆宣扬IoC代码看起来是多么干净时,我一次也没感到惊讶,他们一次谈到C ++模板是回到90年代的一种优雅方式,但是如今,它们却像奥秘一样被谴责。 。ah!


2

在.NET世界中,AOP不太流行,因此对于DI而言,无论您自己编写还是使用其他框架,框架都是您唯一的选择。

如果使用AOP,则可以在编译应用程序时进行注入,这在Java中更为常见。

DI有很多好处,例如减少耦合,因此单元测试更加容易,但是您将如何实现呢?您要自己使用反射吗?


新的MEF框架仅允许.NET使用此功能,我必须说代码更简洁,更易于理解,使DI概念的理解变得更加容易。
09年

4
@TT:MEF 不是依赖注入容器,与AOP无关。
Mauricio Scheffer

2

所以,已经快三年了,是吗?

  1. 评论IoC框架的人中有50%不了解IoC和IoC框架之间的区别。我怀疑他们是否知道您可以在不部署到应用服务器的情况下编写代码

  2. 如果我们采用最受欢迎的Java Spring框架,那是将IoC配置从XMl移到代码中,现在看起来像这样

`@Configuration公共类AppConfig {

public @Bean TransferService transferService() {
    return new TransferServiceImpl(accountRepository());
}

public @Bean AccountRepository accountRepository() {
    return new InMemoryAccountRepository();
}

}`而且我们需要一个框架来做到这一点,为什么呢?


1

老实说,我发现并没有很多需要IoC容器的情况,大多数情况下,它们只是增加了不必要的复杂性。

如果您只是为了简化对象的构造而使用它,我不得不问,您是否在多个位置实例化了该对象?单身人士会不符合您的需求吗?您是否在运行时更改配置?(切换数据源类型等)。

如果是,那么您可能需要一个IoC容器。如果没有,那么您只是将初始化从开发人员可以轻松看到的地方移开了。

谁说接口总比继承好?假设您正在测试服务。为什么不使用构造函数DI,并使用继承创建依赖项的模拟?我使用的大多数服务仅具有一些依赖性。这样做单元测试这种方式阻止保持一吨的无用的接口,意味着你不具备使用ReSharper的快速查找方法的声明。

我相信,对于大多数实现而言,说IoC容器删除不需要的代码是一个神话。

首先,首先要设置容器。然后,您仍然必须定义每个需要初始化的对象。因此,您不必在初始化时保存代码,而是将其移动(除非多次使用您的对象。作为Singleton更好吗?)。然后,对于以这种方式初始化的每个对象,都必须创建并维护一个接口。

有人对此有任何想法吗?


即使对于小型项目,将配置放入XML中也可以使其更加整洁和模块化-并首次使代码可重复使用(因为它不再被胶污染)。对于适当的软件产品,您可以只部署一个更改的XML文件,即可在完全相同的代码库中配置或交换ShippingCostCalculatorProductUIPresentation,或任何其他策略/实现。
Thomas W

如果将它实施为自愿的话,我认为您会增加更多的复杂性,而不是更少。当您需要在运行时交换某些内容时,它们很棒,但是为什么要添加混淆功能呢?为什么要为那些永不切换的事情保留额外接口的开销?我并不是说不要使用IoC进行测试。一定要使用属性,甚至使用构造函数依赖项注入。您需要使用界面吗?为什么不继承您的测试对象?那不是更干净吗?
2013年

您完全不需要接口即可进行XML配置/ IoC。我发现在一个只有约8页和5或6个服务的小型项目中,其清晰度有了很大的提高。有少混淆,因为服务和控制器不再需要的胶水代码。
Thomas W

1

如果需要集中化依赖项的配置,则可能需要一个IoC容器,以便可以轻松地将其全部替换掉​​。这在TDD中最有意义,在TDD中,许多依赖关系被交换掉了,但是这些依赖关系之间的相互依赖性很小。这样做的代价是在某种程度上混淆了对象构造的控制流程,因此拥有一个组织良好且合理记录的配置非常重要。有理由这样做也是很好的,否则,它仅仅是抽象镀金。我看到它做得很差,以至于它被拖到等效于构造函数的goto语句的位置。


1

就是为什么。该项目称为IOC-with-Ninject。您可以使用Visual Studio下载并运行它。本示例使用Ninject,但所有“ new”语句都位于一个位置,您可以通过更改要使用的绑定模块来完全更改应用程序的运行方式。该示例已设置完毕,因此您可以绑定到服务的模拟版本或真实版本。在小型项目中,这可能无关紧要,但是在大型项目中,这很重要。

显而易见,我所看到的优点是:1)所有 语句位于代码根目录的一个位置。2)一次更改就可以完全重构代码。3)“酷系数”的加分是因为...很好:很酷。:p


1

我将尝试找出为什么从我的角度来看国际奥委会可能不利的地方。

与其他所有内容一样,IOC容器(或者像爱因斯坦所说的I = OC ^ 2)是一个概念,您必须自己决定是否需要在代码中使用它。最近对IOC的时尚狂呼仅仅是时尚。不要迷恋时尚,那是第一。您可以在代码中实现无数种概念。首先,自从我开始编程以来,我就一直在使用依赖项注入,并且在该名称以该名称流行后才学会了该术语本身。依赖控制是一个非常古老的主题,迄今为止,它已经以万亿方式解决了,这取决于什么与什么脱钩。将一切从一切中分离出来是胡说八道。IOC容器的问题在于它试图像实体框架或NHibernate一样有用。虽然一旦必须将任何数据库与系统耦合就必须编写对象关系映射器,但IOC容器并非总是必需的。因此,当IOC容器有用时:

  1. 当您遇到具有许多依赖关系的情况时,您需要进行整理
  2. 当您不关心将代码与第三方产品结合在一起时
  3. 当您的开发人员想要学习如何使用新工具时

1:在您的代码中通常没有那么多依赖,或者在设计初期就意识到它们。当抽象思维到期时,抽象思维很有用。

2:将您的代码与第三方代码耦合是一个HuGe问题。我当时使用的代码已有10多年的历史了,那时的代码是高级和高级概念ATL,COM,COM +等。现在,您无法使用该代码。我的意思是说,先进的概念可以带来明显的优势,但是从长远来看,它已被过时的优势本身所抵消。它只是使所有这些都变得更加昂贵。

3:软件开发足够困难。如果允许一些高级概念植入代码中,则可以将其扩展到无法识别的水平。IOC2有问题。尽管它解耦了依赖关系,但它也解耦了逻辑流。假设您发现了一个错误,需要休息一下以检查情况。与其他高级概念一样,IOC2使其变得更加困难。修复概念中的错误比修复普通代码中的错误要困难得多,因为修复错误时,必须再次遵守概念。(仅举一个例子,C ++ .NET经常更改语法,以至于在重构某些旧版本的.NET之前您需要认真考虑。)那么,IOC的问题是什么?问题在于解决依赖关系。解决的逻辑通常隐藏在IOC2本身中,可能以不常见的方式编写,您需要学习和维护。您的第三方产品会在5年后出现吗?微软不是。

关于IOC2到处都写有“我们知道如何”综合症。这类似于自动化测试。花哨的时间和完美的解决方案乍看之下,您只需将所有测试整夜执行,然后在早晨查看结果即可。一个接一个的公司来解释自动化测试的真正含义真的很痛苦。自动化测试绝对不是减少可以一整夜引入的bug数量以提高产品质量的快速方法。但是,时尚使这一概念变得令人讨厌。IOC2患有相同的综合征。人们认为您需要实施它才能使软件保持良好状态。在最近一次采访中,我被问到是否正在实施IOC2和自动化。那是时尚的标志:公司已经用MFC编写了部分代码,他们不会放弃。

您需要像学习软件中的其他任何概念一样学习IOC2。是否需要使用IOC2的决定是在团队和公司内部。但是,在做出决定之前,必须至少提及上述所有论点。只有看到正面优势大于负面优势,您才能做出积极的决定。

IOC2没什么错,除了它仅解决了它解决的问题并介绍了它引入的问题。没有其他的。但是,违背时尚是非常困难的,他们有汗水的嘴巴,追随者。奇怪的是,当他们的幻想变得很明显时,他们中间没有一个人。软件行业中的许多概念得到了捍卫,因为它们创造了利润,撰写了书籍,举行了会议,制造了新产品。那就是时尚,通常是短暂的。人们一旦发现其他东西,便会完全放弃。IOC2很有用,但它显示出与我所见过的许多其他消失的概念相同的信号。我不知道它是否会生存。没有规则。您认为如果有用,它将生存。不,它不是那样的。一家大型的富裕公司就足够了,这个概念可能会在几周内消失。走着瞧。NHibernate幸存下来,EF位居第二。也许IOC2也将生存。不要忘记,软件开发中的大多数概念都没有什么特别的,它们非常合乎逻辑,简单明了,有时记住当前的命名约定比理解概念本身更加困难。IOC2的知识是否会使开发人员成为更好的开发人员?不可以,因为如果开发人员无法提出与IOC2本质上相似的概念,那么他或她将很难理解IOC2正在解决的问题,使用它看起来似乎是虚假的,他或她可能会开始使用它为了某种政治上的正确。不要忘记,软件开发中的大多数概念都没有什么特别的,它们非常合乎逻辑,简单明了,有时记住当前的命名约定比理解概念本身更加困难。IOC2的知识是否会使开发人员成为更好的开发人员?不可以,因为如果开发人员无法提出与IOC2本质上相似的概念,那么他或她将很难理解IOC2正在解决的问题,使用它看起来似乎是虚假的,他或她可能会开始使用它为了某种政治上的正确。不要忘记,软件开发中的大多数概念都没有什么特别的,它们非常合乎逻辑,简单明了,有时记住当前的命名约定比理解概念本身更加困难。IOC2的知识是否会使开发人员成为更好的开发人员?不可以,因为如果开发人员无法提出与IOC2本质上相似的概念,那么他或她将很难理解IOC2正在解决的问题,使用它看起来似乎是虚假的,他或她可能会开始使用它为了某种政治上的正确。IOC2的知识是否会使开发人员成为更好的开发人员?不可以,因为如果开发人员无法提出与IOC2本质上相似的概念,那么他或她将很难理解IOC2正在解决的问题,使用它看起来似乎是虚假的,他或她可能会开始使用它为了某种政治上的正确。IOC2的知识是否会使开发人员成为更好的开发人员?不可以,因为如果开发人员无法提出与IOC2本质上相似的概念,那么他或她将很难理解IOC2正在解决的问题,使用它看起来似乎是虚假的,他或她可能会开始使用它为了某种政治上的正确。


0

就个人而言,我将IoC用作应用程序的某种结构图(是的,我也更喜欢StructureMap;))。在测试过程中,用Moq实现替换我常用的接口实现很容易。创建测试设置就像对IoC框架进行新的init调用一样简单,用模拟代替任何一个类作为我的测试边界。

这可能不是IoC的目的,但我发现自己最常使用它。




0

我意识到这是一篇比较老的文章,但它似乎仍然很活跃,我想我要提供其他一些尚未提及的观点。

我会说我同意依赖注入的好处,但是我确实更喜欢自己构造和管理对象,使用的模式与Maxm007在他的回答中概述的模式没有什么不同。我发现使用第三方容器存在两个主要问题:

1)让第三方库“自动”管理对象的生命周期可使其获得意想不到的结果。我们发现,尤其是在大型项目中,您可以拥有比预期更多的对象副本,并且比手动管理生命周期所拥有的副本还要多。我确信这取决于所使用的框架,但是问题仍然存在。如果您的对象拥有资源,数据连接等,这也会引起问题,因为该对象的寿命有时会比您预期的更长。因此,不可避免地,IoC容器往往会增加应用程序的资源利用率和内存占用量。

2)我认为IoC容器是“黑盒编程”的一种形式。我发现,特别是我们经验不足的开发人员往往会滥用它们。它使程序员不必考虑对象之间的关系或如何解耦,因为它为对象提供了一种机制,使他们可以简单地凭空获取任何想要的对象。例如,可能有很好的设计理由,ObjectA永远不应该直接了解ObjectB,而没有创建工厂,网桥或服务定位器,没有经验的程序员只会说:“没问题,我只是从IoC容器中获取ObjectB。 ”。实际上,这可能导致对象耦合增加,这是IoC可以防止的。


-8

ASP.NET项目中的依赖注入可以用几行代码来完成。我想当您的应用程序使用多个前端并需要进行单元测试时,使用容器会有一些优势。


1
你能给个例子吗?与其他任何应用程序类型相比,ASP.NET中的DI有何不同?
tofi9
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.