我试图提高IoC容器的性能。我们使用的是Unity和SimpleInjector,我们有一个带有这个构造函数的类:
public AuditFacade(
IIocContainer container,
Func<IAuditManager> auditManagerFactory,
Func<ValidatorFactory> validatorCreatorFactory,
IUserContext userContext,
Func<ITenantManager> tenantManagerFactory,
Func<IMonitoringComponent> monitoringComponentFactory)
: base(container, auditManagerFactory, GlobalContext.CurrentTenant,
validatorCreatorFactory, userContext, tenantManagerFactory)
{
_monitoringComponent = new Lazy<IMonitoringComponent>(monitoringComponentFactory);
}
我还有另一个带有这个构造函数的类:
public AuditTenantComponent(Func<IAuditTenantRepository> auditTenantRepository)
{
_auditTenantRepository = new Lazy<IAuditTenantRepository>(auditTenantRepository);
}
我看到第二个在大多数时间以1毫秒的速度被解析,而第一个平均花费50-60毫秒。我确定较慢的原因是由于参数的原因,它有更多的参数。但是,如何才能提高速度较慢的性能呢?事实是我们使用Func<T>
作为参数吗?如果它导致缓慢,我可以改变什么?
答案 0 :(得分:9)
您当前的设计可能需要改进很多。这些改进可以分为五类:
普遍的共识是你应该更喜欢composition over inheritance。与使用组合相比,继承经常被过度使用并且通常会增加复杂性。通过继承,派生类与基类实现紧密耦合。我经常看到一个基类被用作实用的实用程序类,其中包含各种帮助方法,用于横切关注点和某些派生类可能需要的其他行为。
通常更好的方法是一起删除基类,并将服务注入实现(在您的情况下为AuditFacade
类),只暴露服务所需的功能。或者在涉及交叉问题的情况下,根本不要注意这种行为,而是用decorator包含实施,以扩展类的行为,并考虑到跨领域的问题。
在你的情况下,我认为复杂性正在发生,因为实现中没有使用7个注入的依赖项中的6个,而是仅传递给基类。换句话说,这6个依赖项是基类的实现细节,而实现仍然被迫了解它们。通过抽象(部分)服务后面的基类,可以最小化AuditFacade
对两个依赖项所需的依赖项数量:Func<IMonitoringComponent>
和新抽象。该抽象背后的实现将具有6个构造函数依赖项,但AuditFacade
(以及其他实现)对此无视。
AuditFacade
取决于IIocContainer
抽象,这非常类似于Service Locator pattern的实现。 Service Locator should be considered an anti-pattern因为:
它隐藏了一个班级&#39;依赖项,导致运行时错误而不是 编译时错误,以及使代码更难 维持,因为你不清楚什么时候你会介绍一个 打破变革。
将容器或抽象注入应用程序代码总是有更好的替代方法。请注意,有时您可能希望将容器注入工厂实现,但只要将它们放在Composition Root内,就不会有任何危害,因为Service Locator is about roles, not mechanics。< / p>
静态GlobalContext.CurrentTenant
属性是Ambient Context反模式的实现。 Mark Seemann我在our book中写了这个模式:
AMBIENT CONTEXT的问题与SERVICE的问题有关 定位器。主要问题是:
- 隐藏依赖性。
- 测试变得更加困难。
- 根据其上下文更改DEPENDENCY变得非常困难。 [第5.3.3段]
在这种情况下的使用实际上是非常奇怪的IMO,因为您从构造函数内部的某些静态属性中获取当前租户以将其传递给基类。为什么基类不会调用该属性本身?
但没有人应该称之为静态属性。使用这些静态属性会使您的代码更难以阅读和维护。它使得单元测试更加困难,并且由于你的代码库通常会被调用这样的静态,它变成了隐藏的依赖;它与使用服务定位器具有相同的缺点。
Leaky Abstraction违反了Dependency Inversion Principle,抽象违反了原则的第二部分,即:
B中。抽象不应该依赖于细节。细节应该取决于 抽象。
虽然Lazy<T>
本身不是抽象(Lazy<T>
是具体类型),但当用作构造函数参数时,它可能变成漏洞抽象。例如,如果您直接注入Lazy<IMonitoringComponent>
而不是IMonitoringComponent
(这是您在代码中基本上执行的操作),则新的Lazy<IMonitoringComponent>
依赖项会泄漏实现细节。此Lazy<IMonitoringComponent>
向消费者传达了所使用的IMonitoringComponent
实现创建成本高昂或耗时。但为什么消费者会关心这个呢?
但是这方面还有更多问题。如果在某个时间点使用的IUserContext
实现变得昂贵,我们必须开始在整个应用程序中进行彻底的更改(违反Open/Closed Principle),因为所有IUserContext
依赖项都需要更改为Lazy<IUserContext>
,并且必须将IUserContext
的所有消费者更改为使用userContext.Value.
。而且您还必须更改所有单元测试。如果您忘记将IUserContext
引用更改为Lazy<IUserContext>
,或者在创建新课时意外依赖IUserContext
,会发生什么?您的代码中存在错误,因为此时会立即创建用户上下文实现,这会导致性能问题(这会导致问题,因为这是您首先使用Lazy<T>
的原因)。
那么为什么我们正在对我们的代码库进行彻底的更改并使用额外的间接层来污染它?没有理由这样做。创建依赖关系的成本是 实现细节 。你应该把它隐藏在抽象之后。这是一个例子:
public class LazyMonitoringComponentProxy : IMonitoringComponent {
private Lazy<IMonitoringComponent> component;
public LazyMonitoringComponentProxy(Lazy<IMonitoringComponent> component) {
this.component = component;
}
void IMonitoringComponent.MonitoringMethod(string someVar) {
this.component.Value.MonitoringMethod(someVar);
}
}
在此示例中,我们隐藏了proxy class后面的Lazy<IMonitoringComponent>
。这样,我们就可以使用此IMonitoringComponent
替换原始LazyMonitoringComponentProxy
实现,而无需对其他应用程序进行任何更改。使用Simple Injector,我们可以按如下方式注册此类型:
container.Register<IMonitoringComponent>(() => new LazyMonitoringComponentProxy(
new Lazy<IMonitoringComponent>(container.GetInstance<CostlyMonitoringComp>));
正如Lazy<T>
可以被滥用为漏洞抽象一样,Func<T>
同样适用,特别是当你出于性能原因而这样做时。正确应用DI时,大多数情况下无需将工厂抽象注入代码,例如Func<T>
。
请注意,如果您在所有地方注入Lazy<T>
和Func<T>
,则会使您不必要的代码库变得复杂。
但除了Lazy<T>
和Func<T>
漏洞抽象之外,你需要它们的事实表明你的应用程序存在问题,因为Injection Constructors should be simple。如果构造函数需要很长时间才能运行,那么构造函数就会做得太多。构造函数逻辑通常很难测试,如果这样的构造函数调用数据库或从HttpContext请求数据,verification of your object graphs变得更加困难,你可能会一起跳过验证。跳过对象图的验证是一件非常糟糕的事情,因为这会强制您单击整个应用程序以确定您的DI容器是否配置正确。
我希望这可以为您提供有关改进课程设计的一些想法。
答案 1 :(得分:2)
您可以挂钩Simple Injector的管道并添加分析,这样您就可以发现哪些类型的创建速度很慢。这是您可以使用的扩展方法:
public struct ProfileData {
public readonly ExpressionBuildingEventArgs Info;
public readonly TimeSpan Elapsed;
public ProfileData(ExpressionBuildingEventArgs info, TimeSpan elapsed) {
this.Info = info;
this.Elapsed = elapsed;
}
}
static void EnableProfiling(Container container, List<ProfileData> profileLog) {
container.ExpressionBuilding += (s, e) => {
Func<Func<object>, object> profilingWrapper = creator => {
var watch = Stopwatch.StartNew();
var instance = creator.Invoke();
profileLog.Add(new ProfileData(e, watch.Elapsed));
return instance;
};
Func<object> instanceCreator =
Expression.Lambda<Func<object>>(e.Expression).Compile();
e.Expression = Expression.Convert(
Expression.Invoke(
Expression.Constant(profilingWrapper),
Expression.Constant(instanceCreator)),
e.KnownImplementationType);
};
}
您可以按如下方式使用:
var container = new Container();
// TODO: Your registrations here.
// Hook the profiler
List<ProfileData> profileLog = new List<ProfileData>(1000);
// Call this after all registrations.
EnableProfiling(container, profileLog);
// Trigger verification to allow everything to be precompiled.
container.Verify();
profileLog.Clear();
// Resolve a type:
container.GetInstance<AuditFacade>();
// Display resolve time in order of time.
var slowestFirst = profileLog.OrderByDescending(line => line.Elapsed);
foreach (var line in slowestFirst)
{
Console.WriteLine(string.Format("{0} ms: {1}",
line.Info.KnownImplementationType.Name,
line.Elapsed.TotalMilliseconds);
}
请注意,显示的时间包括解析依赖项所需的时间,但这可能会让您非常轻松地导致延迟的类型。
我想在这里注意给定代码有两个重要的事项:
所以不要在生产环境中使用它。
答案 2 :(得分:0)
您所做的一切都与此相关。 通常,递归解析的更多构造函数参数更长而不是更少的参数。但是你必须决定成本是好还是太高。
在你的情况下,50毫秒会导致瓶颈吗?你只创建了一个实例,或者你是在一个紧凑的循环中掏出它们?只比较1毫秒和50毫秒可能会导致你谴责较慢的一个,但如果用户不能告诉你50毫秒通过并且它不会导致应用程序中的其他地方出现问题,为什么要通过箍来加快速度如果你不知道它会被用来吗?