关于c#:Dependency Inject(DI)“友好”库

Dependency Inject (DI) “friendly” library

我正在考虑C库的设计,它将具有几个不同的高级功能。当然,这些高级功能将尽可能使用实体类设计原则来实现。因此,可能会有一些类专门供消费者定期直接使用,"支持类"是那些更常见的"最终用户"类的依赖项。

问题是,设计图书馆的最佳方法是什么?

  • DI不可知-尽管为一个或两个通用DI库(structuremap、ninject等)添加基本的"支持"似乎是合理的,但我希望用户能够将该库与任何DI框架一起使用。
  • 不可DI使用-如果库的使用者不使用DI,那么库应该仍然尽可能容易使用,减少用户创建所有这些"不重要"依赖项所需的工作量,以获得他们想要使用的"真实"类。

我目前的想法是为公共DI库(例如,structuremap注册表、ninject模块)提供一些"DI注册模块",以及一个非DI的集合类或工厂类,其中包含到这些少数工厂的耦合。

思想?


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

要以不可知DI容器的方式设计API,请遵循以下一般原则:

程序到接口,而不是实现

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

运用好莱坞原则

好莱坞的DI原则是:不要叫DI容器,它会叫你。

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

使用构造函数注入

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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; }
    }
}

注意服务类如何保证其不变量。创建实例后,由于guard子句和readonly关键字的组合,依赖项保证可用。

如果需要短期对象,请使用抽象工厂

通过构造函数注入的依赖项往往是长寿命的,但有时您需要一个短命的对象,或者基于仅在运行时已知的值构造依赖项。

有关详细信息,请参阅此。

仅在最后负责的时刻撰写

将对象分离到最后。通常,您可以等待并连接应用程序入口点中的所有内容。这被称为复合根。

详细信息如下:

  • 我应该在哪里使用Ninject2+进行注入(以及如何安排模块?)
  • 设计-使用温莎时应在何处登记物体

简化使用立面

如果您觉得所得到的API对于新手来说过于复杂,那么您总是可以提供一些外观类来封装常见的依赖项组合。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
    }
}

这将允许用户通过编写

1
var foo = new MyFacade().CreateFoo();

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

1
var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

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

在写下这个答案很久之后,我扩展了本文的概念,写了一篇关于DI友好库的较长的博客文章,以及一篇关于DI友好框架的配套文章。


术语"依赖注入"与IOC容器一点关系都没有,即使您倾向于看到它们一起提到。它只是意味着,不要像这样编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
    }
}

你这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; }
}

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

  • 每当您认为可能需要更改实现时,都依赖接口而不是类;

  • 不是在类内创建这些接口的实例,而是将它们作为构造函数参数传递(或者,它们可以分配给公共属性;前者是构造函数注入,后者是属性注入)。

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

    如果您正在寻找一个这样的例子,只需查看.NET框架本身:

    • List执行IList。如果将类设计为使用IList(或IEnumerable),则可以利用诸如延迟加载之类的概念,如linq to sql、linq to entities和nhibernate,通常都是通过属性注入实现的。一些框架类实际上接受一个IList作为构造函数参数,例如BindingList,它用于多个数据绑定功能。

    • linq-to-sql和ef完全围绕IDbConnection和相关接口构建,这些接口可以通过公共构造函数传入。不过,您不需要使用它们;默认的构造器只需将连接字符串放在配置文件中的某个地方就可以正常工作。

    • 如果您曾经使用过WinForms组件,那么您将处理"服务",如INameCreationServiceIExtenderProviderService。你甚至不知道具体的类是什么。.NET实际上有自己的IOC容器IContainer用于此目的,Component类有一个GetService方法,它是实际的服务定位器。当然,没有任何东西可以阻止您在没有IContainer或特定定位器的情况下使用任何或所有这些接口。服务本身仅与容器松散耦合。

    • WCF中的契约完全围绕接口构建。实际的具体服务类通常由配置文件中的名称引用,该文件本质上是DI。许多人没有意识到这一点,但完全有可能用另一个IOC容器替换这个配置系统。也许更有趣的是,服务行为都是IServiceBehavior的实例,可以稍后添加。同样,您可以很容易地将其连接到IOC容器中并让它选择相关的行为,但是如果没有这样的行为,这个特性是完全可用的。

    等等。你会发现在.NET中到处都是DI,只是通常情况下它是如此无缝地完成,以至于你甚至不认为它是DI。

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


    编辑:时间已经过去了,我现在意识到这是一个巨大的错误。IOC容器很糟糕,DI是处理副作用的非常糟糕的方法。实际上,这里的所有答案(以及问题本身)都应该避免。只需注意副作用,将它们与纯粹的代码分离,其他的事情要么就就位了,要么就是不相关和不必要的复杂性。

    原始答案如下:

    我在开发Solrnet时不得不面对同样的决定。我开始的目标是成为DI友好型和容器不可知型的,但是随着我添加越来越多的内部组件,内部工厂很快变得无法管理,结果产生的库是不灵活的。

    最后,我编写了自己的非常简单的嵌入式IOC容器,同时还提供了一个温莎设施和一个Ninject模块。将库与其他容器集成只是正确连接组件的问题,因此我可以轻松地将它与autopac、unity、structuremap等集成。

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

    有关详细信息,请参阅此日志。

    大众运输似乎也依赖类似的东西。它有一个IObjectBuilder接口,该接口实际上是CommonServiceLocator的IServicelocator,使用了更多的方法,然后它为每个容器实现这个接口,即NinObjectBuilder和一个常规模块/设施,即MassTransitModule。然后它依赖于IObjectBuilder来实例化它需要的东西。当然,这是一种有效的方法,但就我个人而言,我不太喜欢它,因为它实际上在容器周围传递了太多信息,将其用作服务定位器。

    单轨铁路也实现了自己的集装箱,这实现了良好的旧ISeviceProvider。此容器通过公开已知服务的接口在整个框架中使用。为了得到具体的容器,它有一个内置的服务提供者定位器。温莎设施将此服务提供商定位器指向温莎,使其成为选定的服务提供商。

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


    我要做的是以一种DI容器不可知论的方式设计我的库,以尽可能地限制对容器的依赖。这允许在DI容器上交换另一个容器(如果需要)。

    然后向库的用户公开DI逻辑之上的层,以便他们可以使用您通过接口选择的任何框架。这样,他们仍然可以使用您公开的DI功能,并且可以自由地为自己的目的使用任何其他框架。

    允许库的用户插入自己的DI框架对我来说似乎有点错误,因为它显著地增加了维护量。这也会比直接DI更像是一个插件环境。