“友好”的依赖注入(DI)图书馆

发布于 2024-08-17 20:56:48 字数 1700 浏览 3 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

指尖凝香 2024-08-24 20:56:48

一旦您了解 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 Clause 和 readonly 关键字的组合,依赖关系保证可用。

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

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

请参阅此 了解更多信息。

仅在最后负责的时刻进行组合

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

更多详细信息请参见:

使用 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);
    }
}

这将允许用户通过编写

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

It 来创建默认的 Foo,但是,很容易发现可以提供自定义依赖项,并且您可以编写

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

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


FWIW,在写完这个答案很久之后,我扩展了这里的概念,并写了一篇关于 DI 友好的库,以及关于 DI 友好的框架

This is actually simple to do once you understand that DI is about patterns and principles, not technology.

To design the API in a DI Container-agnostic way, follow these general principles:

Program to an interface, not an implementation

This principle is actually a quote (from memory though) from Design Patterns, but it should always be your real goal. DI is just a means to achieve that end.

Apply the Hollywood Principle

The Hollywood Principle in DI terms says: Don't call the DI Container, it'll call you.

Never directly ask for a dependency by calling a container from within your code. Ask for it implicitly by using Constructor Injection.

Use Constructor Injection

When you need a dependency, ask for it statically through the constructor:

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

Notice how the Service class guarantees its invariants. Once an instance is created, the dependency is guaranteed to be available because of the combination of the Guard Clause and the readonly keyword.

Use Abstract Factory if you need a short-lived object

Dependencies injected with Constructor Injection tend to be long-lived, but sometimes you need a short-lived object, or to construct the dependency based on a value known only at run-time.

See this for more information.

Compose only at the Last Responsible Moment

Keep objects decoupled until the very end. Normally, you can wait and wire everything up in the application's entry point. This is called the Composition Root.

More details here:

Simplify using a Facade

If you feel that the resulting API becomes too complex for novice users, you can always provide a few Facade classes that encapsulate common dependency combinations.

To provide a flexible Facade with a high degree of discoverability, you could consider providing Fluent Builders. Something like this:

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

This would allow a user to create a default Foo by writing

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

It would, however, be very discoverable that it's possible to supply a custom dependency, and you could write

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

If you imagine that the MyFacade class encapsulates a lot of different dependencies, I hope it's clear how it would provide proper defaults while still making extensibility discoverable.


FWIW, long after writing this answer, I expanded upon the concepts herein and wrote a longer blog post about DI-Friendly Libraries, and a companion post about DI-Friendly Frameworks.

眼趣 2024-08-24 20:56:48

术语“依赖注入”与 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; }
}

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

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

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

所有这些都不以任何 DI 库的存在为前提,并且如果没有 DI 库,它也不会真正使代码编写变得更加困难。

如果您正在寻找这样的示例,只需看看 .NET Framework 本身即可:

  • List 实现 IList。如果您将类设计为使用 IList(或 IEnumerable),则可以利用延迟加载等概念,如 Linq to SQL、Linq实体和 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 Framework 本身的一部分。

The term "dependency injection" doesn't specifically have anything to do with an IoC container at all, even though you tend to see them mentioned together. It simply means that instead of writing your code like this:

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

You write it like this:

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

That is, you do two things when you write your code:

  1. Rely on interfaces instead of classes whenever you think that the implementation might need to be changed;

  2. Instead of creating instances of these interfaces inside a class, pass them as constructor arguments (alternatively, they could be assigned to public properties; the former is constructor injection, the latter is property injection).

None of this presupposes the existence of any DI library, and it doesn't really make the code any more difficult to write without one.

If you're looking for an example of this, look no further than the .NET Framework itself:

  • 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.

And so on and so forth. You'll find DI all over the place in .NET, it's just that normally it's done so seamlessly that you don't even think of it as DI.

If you want to design your DI-enabled library for maximum usability then the best suggestion is probably to supply your own default IoC implementation using a lightweight container. IContainer is a great choice for this because it's a part of the .NET Framework itself.

一身骄傲 2024-08-24 20:56:48

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

原始答案如下:


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

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

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

更多详细信息,请参阅这篇博客文章

MassTransit 似乎也依赖类似的东西。它有一个 IObjectBuilder 接口,实际上是 CommonServiceLocator 的 IServiceLocator,具有更多方法,然后它为每个容器实现此功能,即 NinjectObjectBuilder 和常规模块/设施,即 MassTransitModule。然后依靠 IObjectBuilder 来实例化它所需要的内容。这当然是一种有效的方法,但我个人不太喜欢它,因为它实际上在容器中传递太多,将其用作服务定位器。

MonoRail 实现 它自己的容器,它实现了很好的旧 IServiceProvider。该容器通过 公开已知服务的接口。要获取具体容器,它有一个 内置服务提供商定位器温莎设施 将此服务提供商定位器指向 Windsor,使其成为选定的服务提供商。

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

EDIT 2015: time has passed, I realize now that this whole thing was a huge mistake. IoC containers are terrible and DI is a very poor way to deal with side effects. Effectively, all of the answers here (and the question itself) are to be avoided. Simply be aware of side effects, separate them from pure code, and everything else either falls into place or is irrelevant and unnecessary complexity.

Original answer follows:


I had to face this same decision while developing SolrNet. I started with the goal of being DI-friendly and container-agnostic, but as I added more and more internal components, the internal factories quickly became unmanageable and the resulting library was inflexible.

I ended up writing my own very simple embedded IoC container while also providing a Windsor facility and a Ninject module. Integrating the library with other containers is just a matter of properly wiring the components, so I could easily integrate it with Autofac, Unity, StructureMap, whatever.

The downside of this is that I lost the ability to just new up the service. I also took a dependency on CommonServiceLocator which I could have avoided (I might refactor it out in the future) to make the embedded container easier to implement.

More details in this blog post.

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 implements its own container as well, which implements good old IServiceProvider. This container is used throughout this framework through an interface that exposes well-known services. To get the concrete container, it has a built-in service provider locator. The Windsor facility points this service provider locator to Windsor, making it the selected service provider.

Bottom line: there is no perfect solution. As with any design decision, this issue demands a balance between flexibility, maintainability and convenience.

春风十里 2024-08-24 20:56:48

我要做的是以与 DI 容器无关的方式设计我的库,以尽可能限制对容器的依赖。如果需要,这允许将 DI 容器替换为另一个容器。

然后将 DI 逻辑之上的层公开给库的用户,以便他们可以使用您通过界面选择的任何框架。这样他们仍然可以使用您公开的 DI 功能,并且可以出于自己的目的自由使用任何其他框架。

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

What I would do is design my library in a DI container agnostic way to limit the dependency on the container as much as possible. This allows to swap out on DI container for another if need be.

Then expose the layer above the DI logic to the users of the library so that they can use whatever framework you chose through your interface. This way they can still use DI functionality that you exposed and they are free to use any other framework for their own purposes.

Allowing the users of the library to plug their own DI framework seems a bit wrong to me as it dramatically increases amount of maintenance. This also then becomes more of a plugin environment than straight DI.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文