dependency injection与策略模式
关于这个话题的讨论很多,但每个人似乎都错过了一个明显的答案。 我想帮助审查这个“明显的”IOC容器解决方案。 各种对话假设运行时选择策略和使用IOC容器。 我将继续这些假设。
我还想补充一个假设,即它不是必须选择的单一策略。 相反,我可能需要检索一个对象图,该对象图在整个图的节点中找到了几个策略。
我将首先快速概述两个常用的解决方案,然后我将展示我希望看到IOC容器支持的“明显”替代方案。 我将使用Unity作为示例语法,但我的问题不是Unity特有的。
命名绑定
这种方法要求每个新策略都手动添加绑定:
Container.RegisterType(); Container.RegisterType("Alpha"); Container.RegisterType("Beta");
……然后明确要求正确的策略:
var strategy = Container.Resolve("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();
- 优点:
- IOC容器对消费者隐藏
- “环境背景”更接近预期的结果,但……
- 缺点:
- 每种策略的构造者可能有不同的需求。 但是现在构造函数注入的责任已从容器转移到抽象工厂。 换句话说,每次添加新策略时,可能需要修改相应的抽象工厂。
- 大量使用策略意味着大量创建抽象工厂。 如果IOC容器只是给了一些帮助,那就太好了。
- 如果这是一个multithreading应用程序并且“环境上下文”确实由线程本地存储提供,那么当对象使用注入的抽象工厂来创建它所需的类型时,它可能正在运行不同的线程,不再能访问必要的线程本地存储值。
类型切换/动态绑定
这是我想要使用的方法,而不是上述两种方法。 它涉及提供委托作为IOC容器绑定的一部分。 大多数IOC容器都具有此function,但这种特定方法具有重要的细微差别。
语法如下:
Container.RegisterType(typeof(IDataAccess), new InjectionStrategy((c) => { //Access ambient context (pehaps thread-local-storage) to determine //the type of the strategy... Type selectedStrategy = ...; return selectedStrategy; }) );
请注意, InjectionFactory
未返回IDataAccess
的实例。 相反,它返回一个实现IDataAccess
的类型描述。 然后,IOC容器将执行该类型的通常创建和“构建”,其可能包括选择的其他策略。
这与标准的类型到委托绑定形成对比,在Unity的情况下,它的编码如下:
Container.RegisterType(typeof(IDataAccess), new InjectionFactory((c) => { //Access ambient context (pehaps thread-local-storage) to determine //the type of the strategy... IDataAccess instanceOfelectedStrategy = ...; return instanceOfelectedStrategy; }) );
上述实际上接近满足整体需求,但绝对不符合假设的Unity InjectionStrategy
。
专注于第一个样本(使用假设的Unity InjectionStrategy
):
- 优点:
- 隐藏容器
- 无需创建无穷无尽的抽象工厂,或让消费者摆弄它们。
- 每当有新策略可用时,无需手动调整IOC容器绑定。
- 允许容器保留生命周期管理控件。
- 支持纯DI故事,这意味着multithreading应用程序可以使用正确的线程本地存储设置在线程上创建整个对象图。
- 缺点:
- 由于在创建初始IOC容器绑定时策略返回的
Type
不可用,这意味着第一次返回该类型时可能会遇到微小的性能损失。 换句话说,容器必须在现场反映类型以发现它具有的构造函数,以便它知道如何注入它。 该类型的所有后续出现都应该很快,因为容器可以缓存第一次找到的结果。 这不是一个值得一提的“骗局”,但我正在努力进行全面披露。 - ???
- 由于在创建初始IOC容器绑定时策略返回的
是否存在可以按此方式运行的现有IOC容器? 任何人都有Unity自定义注入类来实现这种效果?
据我所知,这个问题是关于运行时选择或几个候选策略之一的映射。
没有理由依赖DI容器来执行此操作,因为至少有三种方法以容器无关的方式执行此操作:
- 使用元数据角色提示
- 使用角色界面角色提示
- 使用部分类型名称角色提示
我个人的偏好是部分类型名称角色提示。
这是一个迟到的反应,但也许它会帮助其他人。
我有一个非常简单的方法。 我只是创建一个StrategyResolver而不是直接依赖Unity。
public class StrategyResolver : IStrategyResolver { private IUnityContainer container; public StrategyResolver(IUnityContainer unityContainer) { this.container = unityContainer; } public T Resolve(string namedStrategy) { return this.container.Resolve (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(dto.SomeProperty); actionHanlder.Handle(dto); } }
注册:
container.RegisterType("One"); container.RegisterType("Two"); container.RegisterType(); container.RegisterType();
现在,关于这一点的好处是,在将来添加新策略时,我永远不会再次触及StrategyResolver。
这很简单。 非常干净,我将Unity的依赖性保持在最低限度。 我唯一一次触及StrategyResolver是因为我决定改变容器技术,这是不太可能发生的。
希望这可以帮助!
在过去的几年里,我以多种forms实现了这一要求。 首先让我们来看看你在post中看到的要点
假设运行时选择策略和使用IOC容器…添加假设它不是必须选择的单个策略。 相反,我可能需要检索具有多种策略的对象图… [必须]将调用者绑定到IOC容器…每个新策略必须[不需要]手动添加到绑定列表中。 ..如果IOC容器只是提供了一些帮助,那就太好了。
我已经使用Simple Injector作为我选择的容器已经有一段时间了,这个决定的驱动因素之一是它对generics有广泛的支持。 通过此function,我们将实现您的要求。
我坚信代码应该说明一切,所以我会直接进入……
- 我已经定义了一个额外的类
ContainerResolvedClass
来演示Simple Injector找到正确的实现并成功将它们注入到构造函数中。 这是ContainerResolvedClass
类的唯一原因。 (此类通过result.Handlers
公开为测试目的而注入的处理程序。)
第一个测试要求我们为虚构的类Type1
获得一个实现:
[Test] public void CompositeHandlerForType1_Resolves_WithAlphaHandler() { var container = this.ContainerFactory(); var result = container.GetInstance>(); var handlers = result.Handlers.Select(x => x.GetType()); Assert.That(handlers.Count(), Is.EqualTo(1)); Assert.That(handlers.Contains(typeof(AlphaHandler)), Is.True); }
第二个测试要求我们为虚构的类Type2
获得一个实现:
[Test] public void CompositeHandlerForType2_Resolves_WithAlphaHandler() { var container = this.ContainerFactory(); var result = container.GetInstance>(); var handlers = result.Handlers.Select(x => x.GetType()); Assert.That(handlers.Count(), Is.EqualTo(1)); Assert.That(handlers.Contains(typeof(BetaHandler)), Is.True); }
第三个测试要求我们为虚构的类Type3
获得两个实现:
[Test] public void CompositeHandlerForType3_Resolves_WithAlphaAndBetaHandlers() { var container = this.ContainerFactory(); var result = container.GetInstance>(); var handlers = result.Handlers.Select(x => x.GetType()); Assert.That(handlers.Count(), Is.EqualTo(2)); Assert.That(handlers.Contains(typeof(AlphaHandler)), Is.True); Assert.That(handlers.Contains(typeof(BetaHandler )), Is.True); }
这些测试似乎符合您的要求,最重要的是没有容器在解决方案中受到损害 。
诀窍是使用参数对象和标记接口的组合。 参数对象包含行为的数据(即IHandler
的),标记接口控制哪些行为作用于哪些参数对象。
以下是标记接口和参数对象 – 您将注意到Type3
标记有两个标记接口:
private interface IAlpha { } private interface IBeta { } private class Type1 : IAlpha { } private class Type2 : IBeta { } private class Type3 : IAlpha, IBeta { }
以下是行为( IHandler
):
private interface IHandler { } private class AlphaHandler : IHandler where TAlpha : IAlpha { } private class BetaHandler : IHandler where TBeta : IBeta { }
这是找到开放generics的所有实现的单一方法:
public IEnumerable 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 { public readonly IEnumerable> Handlers; public ContainerResolvedClass(IEnumerable> handlers) { this.Handlers = handlers; } }
我意识到这个post很长,但我希望它清楚地展示了你的问题的可能解决方案……
我通常使用Abstract Factory和Named Bindings选项的组合。 在尝试了许多不同的方法后,我发现这种方法是一个不错的平衡。
我所做的是创建一个基本上包装容器实例的工厂。 请参阅Mark的文章 “ 基于容器的工厂”一节 。 正如他所说,我将这个工厂作为组合根的一部分。
为了使我的代码更简洁,更少“魔术字符串”,我使用枚举来表示不同的可能策略,并使用.ToString()方法进行注册和解析。
从你的这些方法的缺点:
通常将调用者绑定到IOC容器
在这种方法中,容器在工厂中引用,它是组合根的一部分,因此这不再是一个问题(在我看来)。
。 。 。 并且当然要求呼叫者知道关于策略的某些信息(例如名称“Alpha”)。
必须手动将每个新策略添加到绑定列表中。 此方法不适合处理对象图中的多个策略。 简而言之,它不符合要求。
在某些时候,需要编写代码以确认提供实现的结构(容器,提供者,工厂等)与需要它的代码之间的映射。 除非你想使用纯粹基于惯例的东西,否则我认为你不能解决这个问题。
每种策略的构造者可能有不同的需求。 但是现在构造函数注入的责任已从容器转移到抽象工厂。 换句话说,每次添加新策略时,可能需要修改相应的抽象工厂。
这种方法完全解决了这个问题。
大量使用策略意味着大量创建抽象工厂。[…]
是的,每组策略都需要一个抽象工厂。
如果这是一个multithreading应用程序并且“环境上下文”确实由线程本地存储提供,那么当对象使用注入的抽象工厂来创建它所需的类型时,它可能正在运行不同的线程,不再能访问必要的线程本地存储值。
这将不再是一个问题,因为不会使用TLC。
我觉得没有一个完美的解决方案,但这种方法对我来说效果很好。