首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >依赖注入(DI)“友好”库

依赖注入(DI)“友好”库
EN

Stack Overflow用户
提问于 2010-01-12 00:20:36
回答 4查看 46.3K关注 0票数 233

我正在考虑C#库的设计,它将具有几种不同的高级功能。当然,这些高级函数将尽可能使用实心类设计原则来实现。因此,可能会有供消费者定期直接使用的类,以及那些更常见的“最终用户”类的依赖关系的“支持类”。

问题是,设计库的最佳方法是:

  • DI不可知论-虽然为一两个普通DI库(StructureMap、Ninject等)添加基本的“支持”似乎是合理的,但我希望消费者能够使用任何DI框架的库。
  • 不可用的--如果库的使用者没有使用DI,那么库仍然应该尽可能容易使用,从而减少用户创建所有这些“不重要”依赖关系所需的工作量,这样才能到达他们想要使用的“真实”类。

我目前的想法是为常见的DI库提供一些"DI注册模块“(例如,StructureMap注册中心、尼尼微模块),以及一个非DI的集合或工厂类,并包含到这几个工厂的耦合。

有什么想法?

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2010-01-12 08:42:13

一旦您了解到DI是关于模式和原则的,而不是技术,那么这实际上很简单。

要以与DI容器无关的方式设计API,请遵循以下一般原则:

程序到接口,而不是实现

这个原则实际上是引用(虽然是从内存中)从设计模式,但它应该始终是您的真正的目标。迪只是实现这一目标的一种手段。

应用好莱坞原则

好莱坞原则在DI术语中说:不要调用DI容器,它会给您打电话。

永远不要通过在代码中调用容器来直接请求依赖项。通过使用构造器注入隐式地请求它。

使用构造器注入

当需要依赖项时,通过构造函数静态地请求它:

代码语言:javascript
复制
public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

注意Service类是如何保证其不变量的。一旦创建了一个实例,就可以保证依赖项是可用的,这是因为Guard和readonly关键字的组合。

使用抽象工厂,如果您需要一个短暂的对象

使用构造函数注入的依赖项往往寿命较长,但有时您需要一个短暂的对象,或者根据仅在运行时已知的值构造依赖项。

有关详细信息,请参阅

只在最后一个负责任的时刻写作

保持对象的解耦直到结束。通常,您可以等待并连接应用程序入口点中的所有内容。这称为组合根

这里有更多细节:

简化了使用Facade

如果您觉得产生的API对于新手用户来说太复杂了,您可以始终提供一些封装公共依赖组合的门面类。

要提供具有高度可发现性的灵活外观,可以考虑提供流利的构建器。就像这样:

代码语言:javascript
复制
public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

这将允许用户通过以下方式创建默认Foo

代码语言:javascript
复制
var foo = new MyFacade().CreateFoo();

但是,很容易发现它可以提供一个自定义依赖项,并且您可以编写

代码语言:javascript
复制
var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

如果您认为MyFacade类封装了许多不同的依赖项,那么我希望它能够清楚地提供适当的缺省值,同时仍然可以发现可扩展性。

FWIW写完这个答案很久之后,我扩展了这里的概念,写了一篇关于迪友爱图书馆的更长的博客文章,以及一篇关于有利于投资的框架的文章。

票数 371
EN

Stack Overflow用户

发布于 2010-01-12 02:30:26

术语“依赖注入”与IoC容器一点关系都没有,尽管您倾向于将它们放在一起。它只是意味着,与其像这样编写代码,不如:

代码语言:javascript
复制
public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

你这样写:

代码语言:javascript
复制
public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

也就是说,在编写代码时要做两件事:

  1. 每当您认为实现可能需要更改时,依赖于接口而不是类;
  2. 与其在类中创建这些接口的实例,不如将它们作为构造函数参数传递(或者,可以将它们分配给公共属性;前者是构造函数注入,后者是属性注入)。

所有这些都不以任何DI库的存在为前提,而且没有DI库也不会使代码编写起来更加困难。

如果您正在寻找这方面的示例,只需查看.NET框架本身:

  • List<T>实现了IList<T>。如果您将类设计为使用IList<T> (或IEnumerable<T>),则可以利用诸如Linq到SQL、Linq到实体和NHibernate等概念,通常是通过属性注入。一些框架类实际上接受IList<T>作为构造函数参数,例如BindingList<T>,它用于几个数据绑定特性。
  • Linq到SQL和EF完全围绕IDbConnection和相关接口构建,这些接口可以通过公共构造函数传入。不过,您不需要使用它们;在某个配置文件中的连接字符串中,默认构造函数可以正常工作。
  • 如果您曾经处理过WinForms组件,则需要处理“服务”,比如INameCreationServiceIExtenderProviderService。你甚至不知道具体的类是什么。.NET实际上有自己的IoC容器IContainer,用于此目的,而Component类有一个GetService方法,它是实际的服务定位器。当然,没有什么可以阻止您在没有IContainer或特定定位器的情况下使用任何或所有这些接口。服务本身只与容器松散耦合。
  • WCF中的合同完全围绕接口构建。实际的具体服务类通常通过配置文件中的名称引用,该配置文件本质上是DI。许多人没有意识到这一点,但是用另一个IoC容器交换这个配置系统是完全可能的。也许更有趣的是,服务行为都是IServiceBehavior的实例,可以在以后添加。同样,您可以轻松地将其连接到IoC容器中,并让它选择相关的行为,但是如果没有该特性,则完全可用。

以此类推。在.NET中,您会发现DI无处不在,只是通常情况下,它是无缝地完成的,您甚至都不认为它是DI。

如果您希望设计支持DI的库以获得最大的可用性,那么最好的建议可能是使用轻量级容器提供您自己的默认IoC实现。IContainer是一个很好的选择,因为它是.NET框架本身的一部分。

票数 43
EN

Stack Overflow用户

发布于 2010-01-12 01:46:37

编辑2015:时间已经过去了,我现在意识到这一切都是一个巨大的错误。IoC容器非常糟糕,DI是处理副作用的一种非常糟糕的方法。实际上,这里的所有答案(以及问题本身)都是要避免的。只需注意副作用,将它们从纯代码中分离出来,其他一切要么就会就位,要么就会变得无关紧要和不必要的复杂性。

原来的答覆如下:

在开发SolrNet时,我不得不面对同样的决定。我一开始的目标是对DI友好和容器无关,但是随着我添加了越来越多的内部组件,内部工厂很快变得无法管理,由此产生的库变得不灵活。

最后,我编写了自己的非常简单的嵌入式IoC容器,同时提供了一个温莎设施和一个尼尼姆模块。将库与其他容器集成只是一个正确连接组件的问题,所以我可以轻松地将它与Autofac、等集成在一起。

这样做的缺点是我失去了使用new服务的能力。我还使用了一个可以避免的依赖CommonServiceLocator (我可能会在将来重构它),以使嵌入式容器更易于实现。

在这个博客帖子中有更多的细节。

MassTransit似乎依赖类似的东西。它有一个IObjectBuilder接口,实际上是CommonServiceLocator的IServiceLocator,还有几个方法,然后它为每个容器(即NinjectObjectBuilder )和一个常规的模块/工具(即MassTransitModule )实现了这个接口。然后它将依赖IObjectBuilder实例化它所需的内容。当然,这是一种有效的方法,但就我个人而言,我不太喜欢它,因为它实际上是在容器周围传递太多,使用它作为服务定位器。

MonoRail也实现了自己的容器,它实现了很好的旧IServiceProvider。这个容器通过公开知名服务的接口。在整个框架中使用。要获得混凝土容器,它有一个内置服务提供者定位器温莎设施将此服务提供者定位器指向Windsor,使其成为所选的服务提供者。

底线:没有完美的解决方案。与任何设计决策一样,这个问题需要在灵活性、可维护性和便利性之间取得平衡。

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/2045904

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档