Dependency Injection and the Strategy Pattern

前端 未结 5 1480
灰色年华
灰色年华 2020-12-02 23:59

There is an enormous amount of discussion on this topic, but everyone seems to miss an obvious answer. I\'d like help vetting this \"obvious\" IOC container solution. Th

相关标签:
5条回答
  • 2020-12-03 00:07

    I generally use a combination of your Abstract Factory and Named Bindings options. After trying many different approaches, I find this approach to be a decent balance.

    What I do is create a factory that essentially wraps the instance of the container. See the section in Mark's article called Container-based Factory. As he suggests, I make this factory part of the composition root.

    To make my code a little cleaner and less "magic string" based, I use an enum to denote the different possible strategies, and use the .ToString() method to register and resolve.

    From your Cons of these approaches:

    Typically binds the caller to the IOC Container

    In this approach, the container is referenced in the factory, which is part of the Composition Root, so this is no longer an issue (in my opinion).

    . . . and certainly requires the caller to know something about the strategy (such as the name "Alpha").

    Every new strategy must be manually added to the list of bindings. This approach is not suitable for handling multiple strategies in an object graph. In short, it does not meet requirements.

    At some point, code needs to be written to acknowledge the mapping between the structure that provides the implementation (container, provider, factory, etc.) and the code that requires it. I don't think you can get around this unless you want to use something that is purely convention-based.

    The constructors of each strategy might have different needs. But now the responsibility of constructor injection has been transferred to the abstract factory from the container. In other words, every time a new strategy is added it may be necessary to modify the corresponding abstract factory.

    This approach solves this concern completely.

    Heavy use of strategies means heavy amounts of creating abstract factories.[...]

    Yes, you will need one abstract factory for each set of strategies.

    If this is a multi-threaded application and the "ambient context" is indeed provided by thread-local-storage, then by the time an object is using an injected abstract-factory to create the type it needs, it may be operating on a different thread which no longer has access to the necessary thread-local-storage value.

    This will no longer be an issue since TLC will not be used.

    I don't feel that there is a perfect solution, but this approach has worked well for me.

    0 讨论(0)
  • 2020-12-03 00:10

    I have achieved this requirement in many forms over the last couple of years. Firstly let's pull the main points I can see in your post

    assume run-time selection of strategies and the use of an IOC container ... add the assumption that it is not a single strategy that must be selected. Rather, I might need to retrieve an object-graph that has several strategies ... [must not] binds the caller to the IOC Container ... Every new strategy must [not need to] be manually added to the list of bindings ... It would be nice if the IOC container simply gave a little more help.

    I have used Simple Injector as my container of choice for some time now and one of the drivers for this decision is that it has extensive support for generics. It is through this feature that we will implement your requirements.

    I'm a firm believer that the code should speak for itself so I'll jump right in ...

    • I have defined an extra class ContainerResolvedClass<T> to demonstrate that Simple Injector finds the right implementation(s) and successfully injects them into a constructor. That is the only reason for the class ContainerResolvedClass<T>. (This class exposes the handlers that are injected into it for test purposes via result.Handlers.)

    This first test requires that we get one implementation back for the fictional class 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);
    }
    

    This second test requires that we get one implementation back for the fictional class 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);
    }
    

    This third test requires that we get two implementations back for the fictional class 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);
    }
    

    These tests seems to meet your requirements and best of all no containers are harmed in the solution.


    The trick is to use a combination of parameter objects and marker interfaces. The parameter objects contain the data for the behaviour (i.e. the IHandler's) and the marker interfaces govern which behaviours act on which parameter objects.

    Here are the marker interfaces and parameter objects - you'll note that Type3 is marked with both marker interfaces:

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

    Here are the behaviours (IHandler<T>'s):

    private interface IHandler<T> { }
    
    private class AlphaHandler<TAlpha> : IHandler<TAlpha> where TAlpha : IAlpha { }
    private class BetaHandler<TBeta> : IHandler<TBeta> where TBeta : IBeta { }
    

    This is the single method that will find all implementations of an open generic:

    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;
    }
    

    And this is the code that configures the container for our tests:

    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;
    }
    

    And finally, the test class ContainerResolvedClass<>

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

    I realise this post is a quite long, but I hope it clearly demonstrates a possible solution to your problem ...

    0 讨论(0)
  • 2020-12-03 00:12

    This is a late response but maybe it will help others.

    I have a pretty simple approach. I simply create a StrategyResolver to not be directly depending on 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);
        }
    }
    

    Usage:

    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);
        }
    }
    

    Registration:

    container.RegisterType<IActionHandler, ActionOne>("One");
    container.RegisterType<IActionHandler, ActionTwo>("Two");
    container.RegisterType<IStrategyResolver, StrategyResolver>();
    container.RegisterType<ISomeInterface, SomeClass>();
    

    Now, the nice thing about this is that I will never have to touch the StrategyResolver ever again when adding new strategies in the future.

    It's very simple. Very clean and I kept the dependency on Unity to a strict minimum. The only time I would have touch the StrategyResolver is if I decide to change container technology which is very unlikely to happen.

    Hope this helps!

    0 讨论(0)
  • 2020-12-03 00:14

    As far as I can tell, this question is about run-time selection or mapping of one of several candidate Strategies.

    There's no reason to rely on a DI Container to do this, as there are at least three ways to do this in a container-agnostic way:

    • Use a Metadata Role Hint
    • Use a Role Interface Role Hint
    • Use a Partial Type Name Role Hint

    My personal preference is the Partial Type Name Role Hint.

    0 讨论(0)
  • 2020-12-03 00:16

    I'd implement something like this.

    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]
        }
    }
    
    0 讨论(0)
提交回复
热议问题