C# 依赖注入(DI)“友好”库
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2045904/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Dependency Inject (DI) "friendly" library
提问by Pete
I'm pondering the design of a C# library, that will have several different high level functions. Of course, those high-level functions will be implemented using the SOLIDclass design principles as much as possible. As such, there will probably be classes intended for consumers to use directly on a regular basis, and "support classes" that are dependencies of those more common "end user" classes.
我正在考虑 C# 库的设计,它将具有几个不同的高级功能。当然,那些高级功能将尽可能使用SOLID类设计原则来实现。因此,可能会有供消费者定期直接使用的类,以及作为那些更常见的“最终用户”类的依赖项的“支持类”。
The question is, what is the best way to design the library so it is:
问题是,设计库的最佳方法是什么:
- DI Agnostic - Although adding basic "support" for one or two of the common DI libraries (StructureMap, Ninject, etc) seems reasonable, I want consumers to be able to use the library with any DI framework.
- Non-DI usable - If a consumer of the library is using no DI, the library should still be as easy to use as possible, reducing the amount of work a user has to do to create all these "unimportant" dependencies just to get to the "real" classes they want to use.
- DI 不可知 - 尽管为一两个常见 DI 库(StructureMap、Ninject 等)添加基本“支持”似乎是合理的,但我希望消费者能够将库与任何 DI 框架一起使用。
- 非 DI 可用 - 如果库的使用者不使用 DI,则该库仍应尽可能易于使用,从而减少用户创建所有这些“不重要”依赖项所需的工作量,只是为了获得他们想要使用的“真实”类。
My current thinking is to provide a few "DI registration modules" for the common DI libraries (e.g a StructureMap registry, a Ninject module), and a set or Factory classes that are non-DI and contain the coupling to those few factories.
我目前的想法是为常见的 DI 库(例如 StructureMap 注册表、Ninject 模块)提供一些“DI 注册模块”,以及一组非 DI 并包含与这些少数工厂的耦合的工厂类。
Thoughts?
想法?
采纳答案by Mark Seemann
This is actually simple to do once you understand that DI is about patterns and principles, not technology.
一旦您了解 DI 是关于模式和原则,而不是技术,这实际上很容易做到。
To design the API in a DI Container-agnostic way, follow these general principles:
要以与 DI 容器无关的方式设计 API,请遵循以下一般原则:
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.
这个原则实际上是来自Design Patterns的引述(虽然来自记忆),但它应该始终是您的真正目标。DI 只是实现这一目标的一种手段。
Apply the Hollywood Principle
应用好莱坞原则
The Hollywood Principle in DI terms says: Don't call the DI Container, it'll call you.
DI 术语中的好莱坞原则说:不要调用 DI 容器,它会调用你。
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 staticallythrough 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.
请注意 Service 类如何保证其不变量。一旦创建了一个实例,由于 Guard 子句和readonly
关键字的组合,依赖项保证可用。
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 thisfor 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:
更多细节在这里:
- Where should I do Injection with Ninject 2+ (and how do I arrange my Modules?)
- Design - Where should objects be registered when using Windsor
Simplify using a Facade
使用 Facade 简化
If you feel that the resulting API becomes too complex for novice users, you can always provide a few Facadeclasses that encapsulate common dependency combinations.
如果您觉得生成的 API 对于新手用户来说变得过于复杂,您总是可以提供一些封装常见依赖组合的Facade类。
To provide a flexible Facade with a high degree of discoverability, you could consider providing Fluent Builders. Something like this:
要提供具有高度可发现性的灵活 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);
}
}
This would allow a user to create a default Foo by writing
这将允许用户通过编写来创建默认的 Foo
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.
如果您想象 MyFacade 类封装了许多不同的依赖项,我希望很清楚它如何提供适当的默认值同时仍然使可扩展性可发现。
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.
FWIW,在写完这个答案很久之后,我扩展了这里的概念,并写了一篇关于DI-Friendly Libraries的更长的博客文章,以及一篇关于DI-Friendly Frameworks的配套文章。
回答by Igor Zevaka
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.
我要做的是以与 DI 容器无关的方式设计我的库,以尽可能地限制对容器的依赖。如果需要,这允许将 DI 容器换成另一个。
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.
然后将 DI 逻辑之上的层公开给库的用户,以便他们可以使用您通过界面选择的任何框架。通过这种方式,他们仍然可以使用您公开的 DI 功能,并且他们可以出于自己的目的自由使用任何其他框架。
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.
允许库的用户插入他们自己的 DI 框架对我来说似乎有点错误,因为它大大增加了维护量。这也变得更像是一个插件环境而不是直接的 DI。
回答by Mauricio Scheffer
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.
编辑 2015 年:时间过去了,我现在意识到这整件事是一个巨大的错误。IoC 容器很糟糕,而 DI 是一种非常糟糕的处理副作用的方法。实际上,这里的所有答案(以及问题本身)都应该避免。只需注意副作用,将它们与纯代码分开,其他所有内容要么就位,要么变得无关紧要和不必要的复杂性。
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.
在开发SolrNet 时,我不得不面对同样的决定。我一开始的目标是对 DI 友好且与容器无关,但是随着我添加越来越多的内部组件,内部工厂很快变得难以管理,并且生成的库不灵活。
I ended up writing my own very simple embedded IoC containerwhile also providing a Windsor facilityand 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.
我最终编写了自己的非常简单的嵌入式 IoC 容器,同时还提供了一个Windsor 工具和一个Ninject 模块。将库与其他容器集成只是正确连接组件的问题,因此我可以轻松地将它与 Autofac、Unity、StructureMap 等集成。
The downside of this is that I lost the ability to just new
up the service. I also took a dependency on CommonServiceLocatorwhich I could have avoided (I might refactor it out in the future) to make the embedded container easier to implement.
这样做的缺点是我失去了new
启动服务的能力。我还依赖于CommonServiceLocator,我可以避免(我可能在未来重构它)以使嵌入式容器更容易实现。
More details in this blog post.
这篇博文中有更多详细信息。
MassTransitseems to rely on something similar. It has an IObjectBuilderinterface which is really CommonServiceLocator's IServiceLocator with a couple more methods, then it implements this for each container, i.e. NinjectObjectBuilderand a regular module/facility, i.e. MassTransitModule. Then it relies on IObjectBuilderto 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.
MassTransit似乎依赖于类似的东西。它有一个IObjectBuilder接口,它实际上是CommonServiceLocator的 IServiceLocator 和更多方法,然后它为每个容器实现了这个接口,即NinjectObjectBuilder和一个常规模块/设施,即MassTransitModule。然后它依赖 IObjectBuilder来实例化它需要的东西。这当然是一种有效的方法,但我个人不太喜欢它,因为它实际上过多地传递容器,将其用作服务定位器。
MonoRailimplements its own containeras 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 facilitypoints this service provider locator to Windsor, making it the selected service provider.
MonoRail 也实现了它自己的容器,它实现了很好的旧IServiceProvider。这个容器通过一个公开众所周知的服务的接口在整个框架中使用。为了获得具体的容器,它有一个内置的服务提供者定位器。在温莎工厂指出该服务提供商定位温莎,使其成为选择的服务提供商。
Bottom line: there is no perfect solution. As with any design decision, this issue demands a balance between flexibility, maintainability and convenience.
底线:没有完美的解决方案。与任何设计决策一样,这个问题需要在灵活性、可维护性和便利性之间取得平衡。
回答by Aaronaught
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:
术语“依赖注入”与 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
}
}
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:
也就是说,您在编写代码时会做两件事:
Rely on interfaces instead of classes whenever you think that the implementation might need to be changed;
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.
这一切都不以任何 DI 库的存在为先决条件,并且如果没有任何 DI 库,也不会真正使代码更难编写。
If you're looking for an example of this, look no further than the .NET Framework itself:
如果您正在寻找这样的示例,请查看 .NET Framework 本身:
List<T>
implementsIList<T>
. If you design your class to useIList<T>
(orIEnumerable<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 anIList<T>
as a constructor argument, such asBindingList<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
orIExtenderProviderService
. 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 theComponent
class has aGetService
method which is the actual service locator. Of course, nothing prevents you from using any or all of these interfaces without theIContainer
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.
List<T>
实施IList<T>
. 如果您将类设计为使用IList<T>
(或IEnumerable<T>
),则可以利用诸如延迟加载之类的概念,因为 Linq to SQL、Linq to Entities 和 NHibernate 都在幕后进行,通常通过属性注入进行。一些框架类实际上接受 anIList<T>
作为构造函数参数,例如BindingList<T>
用于多个数据绑定功能的 。Linq to SQL 和 EF 完全围绕
IDbConnection
和相关接口构建,可以通过公共构造函数传入。不过,您不需要使用它们;默认构造函数与位于某个配置文件中的连接字符串一起工作得很好。如果你曾经在 WinForms 组件上工作过,你会处理“服务”,比如
INameCreationService
或IExtenderProviderService
。你甚至不知道具体的类是什么。.NET 实际上有它自己的 IoC 容器,IContainer
用于此目的,并且Component
该类有一个GetService
方法,它是实际的服务定位器。当然,没有什么可以阻止您在没有IContainer
特定定位器的情况下使用任何或所有这些接口。服务本身仅与容器松散耦合。WCF 中的契约完全围绕接口构建。实际的具体服务类通常在配置文件中通过名称引用,本质上是 DI。很多人没有意识到这一点,但完全有可能用另一个 IoC 容器替换这个配置系统。也许更有趣的是,服务行为都是
IServiceBehavior
以后可以添加的实例。同样,您可以轻松地将其连接到 IoC 容器并让它选择相关行为,但该功能在没有任何行为的情况下完全可用。
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.
等等等等。您会在 .NET 中发现 DI 无处不在,只是通常它是无缝地完成的,以至于您甚至不会将其视为 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.
如果您想设计启用 DI 的库以获得最大可用性,那么最好的建议可能是使用轻量级容器提供您自己的默认 IoC 实现。 IContainer
是一个很好的选择,因为它是 .NET Framework 本身的一部分。