sDi*_*ima 3 c# performance dependency-injection .net-core
服务定位器被认为是一种反模式。但是,如果它们仅在某些条件下使用,那么在构造函数中获取所有必要的依赖项是否正确?
方法一(服务定位器)
public class MyType
{
public void MyMethod()
{
if (someRareCondition1)
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
}
if (someRareCondition2)
{
var dep2 = Locator.Resolve<IDep2>();
dep2.DoSomething();
}
}
}
Run Code Online (Sandbox Code Playgroud)
方法二(构造函数注入)
public class MyType
{
private readonly IDep1 dep1;
private readonly IDep2 dep2;
public MyType(IDep1 dep1, IDep2 dep2)
{
this.dep1 = dep1;
this.dep2 = dep2;
}
public void MyMethod()
{
if (someRareCondition1)
{
dep1.DoSomething();
}
if (someRareCondition2)
{
dep2.DoSomething();
}
}
}
Run Code Online (Sandbox Code Playgroud)
您可以有许多需要不同依赖项的不同空隙,但仅限于某些情况。使用服务定位器来提高性能和内存是否更好?
在我谈论两种方法之间的性能差异之前,我需要设置舞台并谈论Service Locator 反模式,因为并非对 DI 容器的每个回调都是 Service Locator 的实现。
应用程序代码应阻止对 DI 容器(或对它的抽象)的调用,例如在您的 MVC 控制器内部或业务层的代码部分。来自代码库这些部分的回调可以被视为服务定位器的示例。
另一方面,来自应用程序启动路径部分的回调,也就是组合根,不被视为服务定位器实现。这是因为 Service Locator 模式不仅仅是对 Resolve API 的机械描述,而是对它在您的应用程序中所扮演的角色的描述。这些从组合根内部对容器的调用很好、有益,甚至是您的应用程序运行所必需的。因此,对于我回答的其余部分,我宁愿提到“回调到 DI 容器”而不是“使用服务定位器模式”。
谈到性能,有很多事情需要考虑。我不可能提到所有可能的性能瓶颈和调整,但我会提到我认为在您的问题的上下文中最值得讨论的几件事。
通过回调到容器中延迟解析依赖项是否比构造函数注入更快取决于很多因素。一般来说,我会说在这两种情况下,性能通常都无关紧要,因为对象组合不太可能成为应用程序的性能瓶颈。在大多数情况下,I/O 占用了大部分时间。通常最好将时间花在优化 I/O 上——它会以更少的投资获得更好的性能收益。
也就是说,要意识到的一件事是,DI 容器通常是高度优化的,并且可以在编译构成应用程序对象图的生成代码期间进行优化。但是当你开始通过懒惰地回调容器来分解对象图时,这些优化就会被抛弃。这使得构造函数注入成为一种更优化的方法,与将对象图分成几部分并一个一个地解决它们相比。
如果我使用 Simple Injector(我维护的 DI 容器),例如,它会在开始编译之前对生成的表达式树进行非常积极的优化。这些优化包括:
您的里程显然会有所不同,但大多数 DI Containers 执行某种优化。我不确定内置的 ASP.NET Core DI 容器应用了哪些类型的优化,但 AFAIK 其优化是有限的。
调用容器的Resolve方法会产生开销。至少它会导致从请求的类型到能够为该类型组成图的代码进行字典查找,而对于已解析的依赖项,字典查找往往不会(那么多)发生。但在实践中,调用Resolve往往会进行一些有效性检查和其他所需的逻辑,这会增加此类调用的开销。这是构造函数注入比回调更优化的另一个原因。
现代 DI 容器通常经过优化,因此它们可以轻松解析大对象图(尽管对于某些容器,对象图的大小有限制,尽管该限制通常非常大)。与手动创建相同的对象图(使用普通的旧 C#)相比,它们的开销通常很小(尽管存在差异和例外)。但这只有在您遵循最佳实践以保持注入构造函数简单时才有效。当注入构造函数很简单时,注入仅在部分时间使用的依赖项并不重要。
当您未能遵循此最佳实践时,例如通过使用回调到数据库或执行某些日志记录到磁盘的注入构造函数,对象图解析的性能可能会显着降低。当您处理并不总是使用的组件时,这肯定会很痛苦。这似乎是您问题的上下文。下面是一个有问题的注入构造函数的例子:
// This Injection Constructor does more than just receiving its dependencies.
public OrderShippingService(
ILogger logger, IConfigurationProvider provider)
{
this.logger = logger;
// Here it starts using its dependencies.
logger.LogInfo("Creating OrderShippingService.");
this.config = provider.Load<OrderShippingServiceConfig>();
logger.LogInfo("OrderShippingService Config loaded.");
}
Run Code Online (Sandbox Code Playgroud)
因此,我的建议是:遵循“简单注入构造函数”的最佳实践,并确保注入构造函数只接收和存储它们传入的依赖项。不要在构造函数内部使用依赖项。这种做法在处理仅在部分时间使用的依赖项时也有帮助,因为当这些依赖项快速创建时,问题就会消失,并且与执行回调相比,使用构造函数注入通常仍然更快。
最重要的是,还应遵循其他最佳实践,例如单一职责原则。遵循它可以防止构造函数获得许多依赖项并防止构造函数过度注入代码异味。包含具有许多依赖项的类的对象图往往会变得更大,因此解析速度较慢。但是,在处理那些有时使用的依赖项时,这种最佳实践无济于事。
但是,您可能无法重构这种缓慢的构造函数,这需要您防止它被急切地加载。但在其他情况下,急切加载可能会导致问题。例如,当您的应用程序使用Composites或Mediators时,就会发生这种情况。Composites 和 Mediator 通常包含许多组件,并且可以将传入调用转发到它们的有限子集。尤其是 Mediator,它通常将调用转发到单个组件。例如:
// Component using a mediator abstraction.
public class ShipmentController : Controller
{
private readonly IMediator mediator;
public void ShipOrder(ShipOrderCommand cmd) =>
this.mediator.Execute(cmd);
public void CancelOrder(CancelOrderCommand cmd) =>
this.mediator.Execute(cmd);
}
Run Code Online (Sandbox Code Playgroud)
在上面的代码中,IMediator实现应该将Execute调用转发给知道如何处理所提供命令的组件。在此示例中,ShipmentController将两种不同的命令类型转发给中介。
即使使用简单的注入构造函数,当应用程序包含数百个“处理程序”时,前面的示例可能会导致性能问题,以防这些处理程序本身包含深层对象图,并且每次组合时都重新创建ShipmentController。
以下实现演示了这些性能问题:
class Mediator : IMediator
{
private readonly IHandler[] handlers;
public Mediator(IHandler[] handlers) =>
this.handlers = handlers;
public void Execute<T>(T command) =>
this.handlers.OfType<IHandler<T>>().Single()
.Execute(command);
}
}
Run Code Online (Sandbox Code Playgroud)
在此示例中,所有处理程序都在Mediatoris之前急切地创建,并注入到Mediator的构造函数中,而对 的调用Execute仅从列表中选择一个。当有许多处理程序、需要为每个请求创建并且包含许多自己的依赖项时,这可能会导致性能问题。
为防止出现此性能问题,可以考虑调用回 DI 容器。但是,它不需要 Service Locator 反模式,因为Mediator实现(以及回调)应该驻留在您的 Composition Root 中。一个可能的IMediator实现可能是这样的:
class Mediator : IMediator
{
private readonly IHandler[] handlers;
public Mediator(IHandler[] handlers) =>
this.handlers = handlers;
public void Execute<T>(T command) =>
this.handlers.OfType<IHandler<T>>().Single()
.Execute(command);
}
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,仅从 DI 容器请求相关处理程序,而不是所有处理程序。这意味着此时 DI 容器仅为该特定处理程序创建对象图。
在所有情况下,但是,你应该防止调用从回DI容器内的应用程序代码。我什至会争辩说不要Lazy<T>为有条件使用的依赖项注入 a ,即使某些 DI 容器对此提供支持。这只会使使用者的代码及其测试复杂化,并且很容易忘记应用Lazy<T>到该依赖项的所有构造函数。
相反,创建代理将是更好的方法。该代理将位于 Composition Root 内,并将包装 aLazy<T>或回调到容器中:
// As long as this implementation is placed inside the Composition Root,
// this is -not- an implementation of the Service Locator anti-pattern.
class Mediator : IMediator
{
private readonly Container container;
public Mediator(Container container) =>
this.container = container;
public void Execute<T>(T cmd) =>
this.container.Resolve<IHandler<T>>().Execute(cmd);
}
Run Code Online (Sandbox Code Playgroud)
这个代理使消费者保持IDependency干净并且不知道使用任何机制来延迟依赖的创建。您现在不是注入 的RealDependency消费者,而是IDependency注入DelayedDependencyProxy.
最后但重要的一点:请防止过早的优化。即使容器回调更快,也更喜欢构造函数注入而不是容器回调。如果您怀疑构造函数注入有任何性能瓶颈:测量、测量、测量。并验证瓶颈是否真的存在于对象组合本身,或者存在于您的组件之一的构造函数中。如果修复了,请验证这是否可以显着提升性能,以证明其导致的复杂性增加是合理的。对于大多数应用程序来说,1 毫秒的性能优势并不重要。
| 归档时间: |
|
| 查看次数: |
148 次 |
| 最近记录: |