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

问题是,设计库的最佳方式是什么?

DI Agnostic——尽管为一个或两个常见的DI库(StructureMap, Ninject等)添加基本的“支持”似乎是合理的,但我希望消费者能够在任何DI框架中使用这个库。 非依赖注入可用——如果库的使用者不使用依赖注入,那么库仍然应该尽可能地易于使用,从而减少用户创建所有这些“不重要的”依赖关系以获得他们想要使用的“真正的”类所需的工作量。

我目前的想法是为常见的依赖注入库提供一些“依赖注入注册模块”(例如一个StructureMap注册表,一个Ninject模块),以及一个非依赖注入的集合或工厂类,它们包含到那些少数工厂的耦合。

想法吗?


当前回答

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

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

允许库的用户插入他们自己的DI框架对我来说似乎有点错误,因为它极大地增加了维护工作量。这也更像是一个插件环境,而不是直接依赖注入。

其他回答

术语“依赖注入”与IoC容器没有任何特别的关系,尽管您经常看到它们一起被提到。它只是意味着不要像这样写代码:

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
    }
}

你可以这样写:

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<T> implements IList<T>. If you design your class to use IList<T> (or IEnumerable<T>), you can take advantage of concepts like lazy-loading, as Linq to SQL, Linq to Entities, and NHibernate all do behind the scenes, usually through property injection. Some framework classes actually accept an IList<T> as a constructor argument, such as BindingList<T>, which is used for several data binding features. Linq to SQL and EF are built entirely around the IDbConnection and related interfaces, which can be passed in via the public constructors. You don't need to use them, though; the default constructors work just fine with a connection string sitting in a configuration file somewhere. If you ever work on WinForms components you deal with "services", like INameCreationService or IExtenderProviderService. You don't even really know what what the concrete classes are. .NET actually has its own IoC container, IContainer, which gets used for this, and the Component class has a GetService method which is the actual service locator. Of course, nothing prevents you from using any or all of these interfaces without the IContainer or that particular locator. The services themselves are only loosely-coupled with the container. Contracts in WCF are built entirely around interfaces. The actual concrete service class is usually referenced by name in a configuration file, which is essentially DI. Many people don't realize this but it is entirely possible to swap out this configuration system with another IoC container. Perhaps more interestingly, the service behaviors are all instances of IServiceBehavior which can be added later. Again, you could easily wire this into an IoC container and have it pick the relevant behaviors, but the feature is completely usable without one.

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

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

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

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

允许库的用户插入他们自己的DI框架对我来说似乎有点错误,因为它极大地增加了维护工作量。这也更像是一个插件环境,而不是直接依赖注入。

一旦您理解了依赖注入是关于模式和原则,而不是技术,这实际上很容易做到。

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

针对接口编程,而不是针对实现编程

这个原则实际上引用了《设计模式》中的一句话,但它应该始终是你真正的目标。DI只是实现这一目的的一种手段。

运用好莱坞原则

DI术语中的好莱坞原则是:不要调用DI容器,它会调用你。

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

使用构造函数注入

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

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关键字的组合,就保证了依赖项是可用的。

如果你需要一个短命的对象,请使用抽象工厂

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

更多信息请参见此。

只在最后的责任时刻撰写

直到最后都保持对象的解耦。通常,您可以等待并在应用程序的入口点中将所有内容连接起来。这就是所谓的组合根。

详情如下:

我应该在哪里用Ninject 2+做注入(以及我如何安排我的模块?) 设计-当使用温莎时,应该在哪里注册对象

简化Facade的使用

如果您觉得生成的API对于新手用户来说太复杂了,那么您总是可以提供一些封装公共依赖项组合的Facade类。

要提供具有高度可发现性的灵活Facade,您可以考虑提供Fluent Builders。就像这样:

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

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

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

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

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


FWIW,在写完这个答案很久之后,我扩展了这里的概念,并写了一篇关于DI-Friendly库的更长的博客文章,以及一篇关于DI-Friendly框架的同伴文章。

编辑2015:时间过去了,我现在意识到这整件事是一个巨大的错误。IoC容器非常糟糕,而DI是处理副作用的一种非常糟糕的方式。实际上,这里所有的答案(以及问题本身)都是要避免的。只要意识到副作用,将它们与纯代码分开,其他所有事情要么就迎刃而解,要么就变得无关紧要且不必要的复杂。

原答案如下:


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

我最终编写了自己的非常简单的嵌入式IoC容器,同时还提供了一个Windsor工具和一个Ninject模块。将库与其他容器集成只是正确地连接组件的问题,所以我可以轻松地将其与Autofac、Unity、StructureMap等集成。

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

更多细节在这篇博客文章中。

MassTransit seems to rely on something similar. It has an IObjectBuilder interface which is really CommonServiceLocator's IServiceLocator with a couple more methods, then it implements this for each container, i.e. NinjectObjectBuilder and a regular module/facility, i.e. MassTransitModule. Then it relies on IObjectBuilder to instantiate what it needs. This is a valid approach of course, but personally I don't like it very much since it's actually passing around the container too much, using it as a service locator.

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

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