【问题标题】:Dependency Injection and the Strategy Pattern依赖注入和策略模式
【发布时间】:2014-05-07 05:46:49
【问题描述】:

关于这个话题有大量的讨论,但每个人似乎都错过了一个明显的答案。我想帮助审查这个“显而易见的”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 提供,那么当一个对象使用注入的抽象工厂创建它需要的类型时,它可能是在不再有权访问必要的线程本地存储值的不同线程上运行。

类型切换/动态绑定

这是我想使用的方法,而不是上述两种方法。它涉及提供一个委托作为 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 自定义注入类?

【问题讨论】:

  • 您的Type Switching / Dynamic Binding 示例与Abstract Factory 有何实际不同?您最终将不得不为两者编写几乎相同的代码。只是一个是一堆类,另一个是每个类型的一堆Container.RegisterType调用。

标签: c# .net dependency-injection ioc-container strategy-pattern


【解决方案1】:

据我所知,这个问题是关于几个候选策略之一的运行时选择或映射。

没有理由依赖 DI 容器来执行此操作,因为至少有三种方法可以以与容器无关的方式执行此操作:

我个人的偏好是部分类型名称角色提示。

【讨论】:

  • @MarkSeemann 生成与初始绑定过程没有直接关系的factories 是否被认为是“坏品味”?除此之外:有问题的工厂没有注入Customer 构造函数吗?假设系统启动时也没有Customers,那么.. 在IoC-container-wired系统中是否禁止使用new
  • @jungle_mole 抽象工厂也是一个选项:stackoverflow.com/a/1945023/126014
【解决方案2】:

在过去的几年里,我以多种形式实现了这一要求。首先让我们把我在你的帖子中看到的要点拉出来

假设策略的运行时选择和 IOC 容器的使用...添加它不是必须选择的单一策略的假设。相反,我可能需要检索具有多种策略的对象图……[不得]将调用者绑定到 IOC 容器……必须[无需]手动将每个新策略添加到绑定列表中。 .. 如果 IOC 容器能提供更多帮助就更好了。

一段时间以来,我一直使用Simple Injector 作为我选择的容器,而做出这一决定的驱动因素之一是它广泛支持泛型。正是通过此功能,我们将实现您的要求。

我坚信代码应该自己说话,所以我会直接跳进去......

  • 我定义了一个额外的类ContainerResolvedClass&lt;T&gt; 来证明Simple Injector 找到了正确的实现并成功地将它们注入到构造函数中。这是ContainerResolvedClass&lt;T&gt; 类的唯一原因。 (该类公开了通过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's),标记接口控制哪些行为作用于哪些参数对象。

这里是标记接口和参数对象 - 你会注意到Type3 被两个标记接口标记:

private interface IAlpha { }
private interface IBeta { }

private class Type1 : IAlpha { }
private class Type2 : IBeta { }
private class Type3 : IAlpha, IBeta { }

以下是行为(IHandler&lt;T&gt;'s):

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&lt;&gt;

private class ContainerResolvedClass<T>
{
    public readonly IEnumerable<IHandler<T>> Handlers;

    public ContainerResolvedClass(IEnumerable<IHandler<T>> handlers)
    {
        this.Handlers = handlers;
    }
}

我知道这篇文章很长,但我希望它清楚地展示了解决您问题的可能方法......

【讨论】:

  • 出色的工作,非常感谢。但是,照原样,有两个问题阻止了我的回答。首先,如果我向 ContainerResolvedClass 添加第二个构造函数参数,而第二个参数代表一种不同的“策略”,该怎么办?现在我必须修改ContainerResolvedClass 的定义。其次,为了解决特定的策略,我仍然有效地为 Container.GetInstance(ContainerResolvedClass) 提供了一个(通用)参数“T”。此外,这种方法有一个倾斜的服务定位器。我正在寻找纯DI。我将更新我的问题参数。
  • @BrentArias Container.GetInstance 是测试代码。您只需要在组合根目录中引用容器。我已经通过Type3 测试介绍了 2 种策略的示例,您可以在其中获得一种类型的 2 种行为。我没有专门对此进行编码以适应策略模式,因为它同样适用于观察者和访问者模式。我将其编写为一个通用的代码示例,它将为您提供一个或多个纯粹基于代码/配置的实现,而不需要您的任何类引用组合根以外的容器。
  • 这段代码不会像类型切换一样返回任何内容,在这两种情况下缓解它的方法是使用单例 NullHandler 来回退。
【解决方案3】:

我通常结合使用您的 Abstract FactoryNamed Bindings 选项。在尝试了许多不同的方法后,我发现这种方法是一种不错的平衡。

我所做的是创建一个基本上包装容器实例的工厂。请参阅 Mark 的 article 中称为 Container-based Factory 的部分。正如他所建议的,我将这个工厂作为组合根的一部分。

为了使我的代码更简洁,更少基于“魔术字符串”,我使用枚举来表示不同的可能策略,并使用 .ToString() 方法进行注册和解析。

从这些方法的缺点来看:

通常将调用者绑定到 IOC 容器

在这种方法中,容器是在工厂中引用的,它是 Composition Root 的一部分,所以这不再是问题(在我看来)。

。 . .并且当然需要调用者了解该策略的一些信息(例如 命名为“Alpha”)。

每个新策略都必须手动添加到列表中 的绑定。这种方法不适合处理多个 对象图中的策略。简而言之,它不满足 要求。

在某些时候,需要编写代码来确认提供实现的结构(容器、提供者、工厂等)与需要它的代码之间的映射。除非您想使用纯粹基于约定的东西,否则我认为您无法解决这个问题。

每个策略的构造器可能有不同的需求。但是现在构造函数注入的责任已经从容器转移到了抽象工厂。换句话说,每次添加新策略时,可能都需要修改相应的抽象工厂。

这种方法完全解决了这个问题。

大量使用策略意味着大量创建抽象工厂。[...]

是的,每组策略都需要一个抽象工厂。

如果这是一个多线程应用程序并且“环境上下文”确实由 thread-local-storage 提供,那么当一个对象使用注入的抽象工厂来创建它需要的类型时,它可能在另一个线程上运行,该线程不再有权访问必要的线程本地存储值。

这将不再是问题,因为不会使用 TLC。

我觉得没有完美的解决方案,但这种方法对我来说效果很好。

【讨论】:

    【解决方案4】:

    这是一个迟到的回应,但也许它会帮助其他人。

    我有一个非常简单的方法。我只是创建了一个不直接依赖于 Unity 的 StrategyResolver。

    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 是如果我决定改变容器技术,这不太可能发生。

    希望这会有所帮助!

    【讨论】:

    • 您的解决方案使用了服务定位器,由于围绕用户体验、实体和封装的争论,该服务定位器被认为是一种反模式
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-23
    • 2012-09-14
    相关资源
    最近更新 更多