依赖注入和策略模式
本文关键字:模式 策略 注入 依赖 | 更新日期: 2023-09-27 17:54:08
关于这个话题有大量的讨论,但每个人似乎都错过了一个明显的答案。我想帮助审查这个"显而易见"的IOC容器解决方案。各种对话假定在运行时选择策略并使用IOC容器。我将继续这些假设。
我还想添加一个假设,即它不是必须选择的单一策略。相反,我可能需要检索一个对象图,该对象图在整个图的节点中都有几种策略。
我将首先快速概述两种通常提出的解决方案,然后我将介绍我希望看到IOC容器支持的"明显"替代方案。我将使用Unity作为示例语法,尽管我的问题不是特定于Unity。
命名绑定这种方法要求每个新策略都有一个手动添加的绑定:
Container.RegisterType<IDataAccess, DefaultAccessor>();
Container.RegisterType<IDataAccess, AlphaAccessor>("Alpha");
Container.RegisterType<IDataAccess, BetaAccessor>("Beta");
…然后明确地请求正确的策略:
var strategy = Container.Resolve<IDataAccess>("Alpha");
- 优点:简单,并支持所有IOC容器
- 缺点:
- 通常将调用者绑定到IOC容器,并且当然要求调用者了解有关策略的一些信息(例如名称"Alpha")。
- 每个新策略都必须手动添加到绑定列表中。
- 这种方法不适合处理对象图中的多个策略。总之,不符合要求。
抽象工厂
为了说明这种方法,假设有以下类:public class DataAccessFactory{
public IDataAccess Create(string strategy){
return //insert appropriate creation logic here.
}
public IDataAccess Create(){
return //Choose strategy through ambient context, such as thread-local-storage.
}
}
public class Consumer
{
public Consumer(DataAccessFactory datafactory)
{
//variation #1. Not sufficient to meet requirements.
var myDataStrategy = datafactory.Create("Alpha");
//variation #2. This is sufficient for requirements.
var myDataStrategy = datafactory.Create();
}
}
然后IOC容器有以下绑定:
Container.RegisterType<DataAccessFactory>();
- 优点:
- IOC容器对消费者是隐藏的
- "环境上下文"更接近期望的结果,但是…
- 缺点:
- 每种策略的构建者可能有不同的需求。但是现在构造函数注入的责任已经从容器转移到抽象工厂。换句话说,每次添加新策略时,都可能需要修改相应的抽象工厂。
- 大量使用策略意味着大量创建抽象工厂。如果IOC容器仅仅给一点更多的帮助就好了。
- 如果这是一个多线程应用程序,并且"环境上下文"确实是由thread-local-storage提供的,那么当一个对象使用注入的抽象工厂来创建它需要的类型时,它可能正在另一个线程上运行,而这个线程不再具有访问必要的thread-local-storage值的权限。
类型切换/动态绑定
这是我想用的方法,而不是上面两种方法。它涉及到提供一个委托作为IOC容器绑定的一部分。大多数IOC容器都已经具有这种能力,但是这种特定的方法有一个重要的细微差别。
语法是这样的:
Container.RegisterType(typeof(IDataAccess),
new InjectionStrategy((c) =>
{
//Access ambient context (perhaps thread-local-storage) to determine
//the type of the strategy...
Type selectedStrategy = ...;
return selectedStrategy;
})
);
注意,InjectionStrategy
是而不是返回IDataAccess
的实例。相反,它返回一个实现IDataAccess
的类型描述。然后IOC容器将执行该类型的常规创建和"构建",其中可能包括正在选择的其他策略。
这与标准的类型到委托绑定形成对比,在Unity的情况下,它是这样编码的:
Container.RegisterType(typeof(IDataAccess),
new InjectionFactory((c) =>
{
//Access ambient context (perhaps thread-local-storage) to determine
//the type of the strategy...
IDataAccess instanceOfSelectedStrategy = ...;
return instanceOfSelectedStrategy;
})
);
以上实际上接近于满足总体需求,但绝对低于假设的Unity InjectionStrategy
。
专注于第一个示例(使用假设的Unity InjectionStrategy
):
- 优点:
- 隐藏容器
不需要创建无穷无尽的抽象工厂,也不需要让消费者去摆弄它们。 - 不需要在新策略可用时手动调整IOC容器绑定
- 允许容器保留生命周期管理控制。
- 支持纯DI场景,这意味着一个多线程应用可以在一个线程上创建整个对象图,并使用适当的线程本地存储设置。
- 因为在创建初始IOC容器绑定时,策略返回的
Type
不可用,这意味着在第一次返回该类型时可能会有很小的性能影响。换句话说,容器必须当场反映类型,以发现它有哪些构造函数,从而知道如何注入它。该类型的所有后续出现都应该是快速的,因为容器可以缓存它从第一次找到的结果。这不是一个值得提及的"骗局",但我试图全面披露。 - ? ?
是否有一个现有的IOC容器可以这样做?谁有一个Unity自定义注入类,实现这种效果?
据我所知,这个问题是关于运行时选择或几个候选策略之一的映射。
没有理由依赖DI容器来做这件事,因为至少有三种方法可以以容器无关的方式做到这一点:- 使用元数据角色提示
- 使用角色接口角色提示
- 使用部分类型名角色提示
我的个人偏好是部分类型名称角色提示。
在过去的几年中,我已经以多种形式达到了这个要求。首先,让我们把我在你的文章中看到的要点拉出来
假设在运行时选择策略并使用IOC容器…添加一个假设,即它不是必须选择的单一策略。相反,我可能需要检索具有几种策略的对象图……[绝对不能]将调用者绑定到IOC容器…每个新策略都必须(不需要)手动添加到绑定列表中……如果IOC容器能提供更多的帮助就好了。
我选择Simple Injector作为我的容器已经有一段时间了,做出这个决定的驱动因素之一是它对泛型的广泛支持。我们将通过这个功能实现您的要求。
我坚信代码应该为自己说话,所以我将直接进入…
- 我定义了一个额外的类
ContainerResolvedClass<T>
来演示简单注入器找到正确的实现并成功地将它们注入构造函数。这是ContainerResolvedClass<T>
类存在的唯一原因。(这个类暴露了通过result.Handlers
注入到它中用于测试目的的处理程序。)第一个测试要求我们获得虚构类Type1
的一个实现:
[Test]
public void CompositeHandlerForType1_Resolves_WithAlphaHandler()
{
var container = this.ContainerFactory();
var result = container.GetInstance<ContainerResolvedClass<Type1>>();
var handlers = result.Handlers.Select(x => x.GetType());
Assert.That(handlers.Count(), Is.EqualTo(1));
Assert.That(handlers.Contains(typeof(AlphaHandler<Type1>)), Is.True);
}
第二个测试要求我们获得虚构类Type2
的一个实现:
[Test]
public void CompositeHandlerForType2_Resolves_WithAlphaHandler()
{
var container = this.ContainerFactory();
var result = container.GetInstance<ContainerResolvedClass<Type2>>();
var handlers = result.Handlers.Select(x => x.GetType());
Assert.That(handlers.Count(), Is.EqualTo(1));
Assert.That(handlers.Contains(typeof(BetaHandler<Type2>)), Is.True);
}
第三个测试要求我们获得虚构类Type3
的两个实现:
[Test]
public void CompositeHandlerForType3_Resolves_WithAlphaAndBetaHandlers()
{
var container = this.ContainerFactory();
var result = container.GetInstance<ContainerResolvedClass<Type3>>();
var handlers = result.Handlers.Select(x => x.GetType());
Assert.That(handlers.Count(), Is.EqualTo(2));
Assert.That(handlers.Contains(typeof(AlphaHandler<Type3>)), Is.True);
Assert.That(handlers.Contains(typeof(BetaHandler<Type3>)), Is.True);
}
这些测试似乎符合您的要求,最重要的是没有容器在解决方案中受到损害。
技巧是使用参数对象和标记接口的组合。参数对象包含行为(即
IHandler
)的数据,标记接口控制哪些行为作用于哪些参数对象。
以下是标记接口和参数对象—您将注意到Type3
被标记为两个标记接口:
private interface IAlpha { }
private interface IBeta { }
private class Type1 : IAlpha { }
private class Type2 : IBeta { }
private class Type3 : IAlpha, IBeta { }
以下是行为(IHandler<T>
):
private interface IHandler<T> { }
private class AlphaHandler<TAlpha> : IHandler<TAlpha> where TAlpha : IAlpha { }
private class BetaHandler<TBeta> : IHandler<TBeta> where TBeta : IBeta { }
这是一个方法,可以找到一个开放泛型的所有实现:
public IEnumerable<Type> GetLoadedOpenGenericImplementations(Type type)
{
var types =
from assembly in AppDomain.CurrentDomain.GetAssemblies()
from t in assembly.GetTypes()
where !t.IsAbstract
from i in t.GetInterfaces()
where i.IsGenericType
where i.GetGenericTypeDefinition() == type
select t;
return types;
}
下面是为我们的测试配置容器的代码:
private Container ContainerFactory()
{
var container = new Container();
var types = this.GetLoadedOpenGenericImplementations(typeof(IHandler<>));
container.RegisterAllOpenGeneric(typeof(IHandler<>), types);
container.RegisterOpenGeneric(
typeof(ContainerResolvedClass<>),
typeof(ContainerResolvedClass<>));
return container;
}
最后,测试类ContainerResolvedClass<>
private class ContainerResolvedClass<T>
{
public readonly IEnumerable<IHandler<T>> Handlers;
public ContainerResolvedClass(IEnumerable<IHandler<T>> handlers)
{
this.Handlers = handlers;
}
}
我知道这篇文章很长,但我希望它能清楚地展示一个可能的解决你的问题的方法…
这是一个迟来的回应,但也许它会帮助别人。
我有一个很简单的方法。我只是创建了一个不直接依赖于Unity的策略解析器。
public class StrategyResolver : IStrategyResolver
{
private IUnityContainer container;
public StrategyResolver(IUnityContainer unityContainer)
{
this.container = unityContainer;
}
public T Resolve<T>(string namedStrategy)
{
return this.container.Resolve<T>(namedStrategy);
}
}
用法:
public class SomeClass: ISomeInterface
{
private IStrategyResolver strategyResolver;
public SomeClass(IStrategyResolver stratResolver)
{
this.strategyResolver = stratResolver;
}
public void Process(SomeDto dto)
{
IActionHandler actionHanlder = this.strategyResolver.Resolve<IActionHandler>(dto.SomeProperty);
actionHanlder.Handle(dto);
}
}
注册:container.RegisterType<IActionHandler, ActionOne>("One");
container.RegisterType<IActionHandler, ActionTwo>("Two");
container.RegisterType<IStrategyResolver, StrategyResolver>();
container.RegisterType<ISomeInterface, SomeClass>();
现在,这样做的好处是,在将来添加新策略时,我再也不用碰StrategyResolver了。
这很简单。非常干净,我将对Unity的依赖降到最低。只有当我决定改变容器技术时,我才会接触到StrategyResolver,这是不太可能发生的。
希望这对你有帮助!
我通常使用抽象工厂和命名绑定选项的组合。在尝试了许多不同的方法之后,我发现这种方法是一个不错的平衡。
我所做的是创建一个工厂,本质上包装容器的实例。请参阅Mark文章中的基于容器的工厂一节。按照他的建议,我把这个工厂作为组成根的一部分。
为了使我的代码更简洁,更少地基于"魔法字符串",我使用enum来表示不同的可能策略,并使用. tostring()方法来注册和解析。
这些方法的缺点:
通常将调用者绑定到IOC容器
在这种方法中,容器在工厂中被引用,工厂是组合根的一部分,所以这不再是一个问题(在我看来)。
…当然也需要调用者了解一些策略(比如名"阿尔法")。
每个新策略都必须手动添加到列表中的绑定。这种方法不适合处理多个对象图中的策略。简而言之,它不符合要求。
在某些时候,需要编写代码来确认提供实现的结构(容器、提供者、工厂等)与需要它的代码之间的映射。我不认为你可以绕过这个,除非你想使用一些纯粹基于约定的东西。
每个策略的构造者可能有不同的需求。但是现在构造函数注入的责任已经从容器转移到抽象工厂。换句话说,每次添加新策略时,都可能需要修改相应的抽象工厂。
这个方法完全解决了这个问题。
大量使用策略意味着大量创建抽象工厂[…]
是的,每组策略需要一个抽象工厂。
如果这是一个多线程应用程序,并且"环境上下文"确实是由thread-local-storage提供的,那么当一个对象使用注入的抽象工厂来创建它需要的类型时,它可能正在另一个线程上操作,而这个线程不再具有访问必要的thread-local-storage值的权限。
这将不再是一个问题,因为TLC将不再使用。
我不认为有一个完美的解决方案,但这种方法对我来说很有效。