依赖注入与策略模式

时间:2014-03-27 23:35:42

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

关于这个话题有很多讨论,但每个人似乎都错过了一个明显的答案。我想帮助审查这个“明显的”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容器只是给了更多帮助,那就太好了。
    • 如果这是一个多线程应用程序并且“环境上下文”确实由线程本地存储提供,那么当对象使用注入的抽象工厂来创建它所需的类型时,它可能是在不同的线程上运行,该线程不再具有必要的线程本地存储值。

类型切换/动态绑定

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

5 个答案:

答案 0 :(得分:14)

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

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

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

答案 1 :(得分:1)

这是一个迟到的回应,但也许会有所帮助。

我有一个非常简单的方法。我只是创建一个StrategyResolver而不是直接依赖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是因为我决定改变容器技术,这是不太可能发生的。

希望这有帮助!

答案 2 :(得分:0)

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

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

我已经使用Simple Injector作为我选择的容器已经有一段时间了,这个决定的驱动因素之一就是它对泛型有广泛的支持。通过此功能,我们将实现您的要求。

我坚信代码应该说明一切,所以我会直接进入......

  • 我已经定义了一个额外的类ContainerResolvedClass<T>来证明Simple Injector找到了正确的实现并成功地将它们注入到构造函数中。这是课程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;
    }
}

我意识到这篇文章很长,但我希望它能清楚地展示出你的问题的可能解决方案......

答案 3 :(得分:0)

我通常使用抽象工厂命名绑定选项的组合。在尝试了许多不同的方法后,我发现这种方法是一个不错的平衡。

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

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

从这些方法的缺点:

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

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

。 。 。并且当然要求呼叫者知道关于策略的一些事情(例如    名称“Alpha”)。

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

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

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

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

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

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

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

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

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

答案 4 :(得分:0)

我会实现这样的东西。

public interface IAbstractFactory
{
    IFiledAppSettingsFactory this[Provider provider] { get; }
}

public Enum : int 
{ 
   One =1, Two =2, Three =3
}


internal class AbstractFactory : IAbstractFactory
{
    public AbstractFactory(/** dependencies **/)
    {
    }

    private readonly IReadOnlyDictionary<Provider, IFactory> services
       = new Dictionary<Provider, IFactory>
    {
       { Provider.One , new Factory1(/** dependencies comming from AbstractFactory **/) },
       { Provider.Two , new Factory2(/** no dependencies **/) },
       { Provider.Three, new Factory3(/** maybe more dependencies comming from AbstractFactory **/) },
    };

    IFactory IAbstractFactory.this[Provider provider] => this.services[provider];
}

internal sealed class Factory1: IFactory
{
    internal FiledSelfFactory(/** any dependencies will come from AbstractFactory **/)
    {
    }
}

internal sealed class Factory2: IFactory
{
    internal FiledSelfFactory(/** any dependencies will come from AbstractFactory **/)
    {
    }
}

internal sealed class Factory3: IFactory
{
    internal FiledSelfFactory(/** any dependencies will come from AbstractFactory **/)
    {
    }
}

public static void AddAppSettings(this IServiceCollection serviceDescriptors)
{
    serviceDescriptors.AddSingleton<IAbstractFactory, AbstractFactory>();
}


public class Consumer
{
    private readonly IFactory realFactory;
    public Consumer(IIAbstractFactory factory) 
    {
          realFactory = factory[Provider.One]
    }
}