How to avoid Service Locator Anti-Pattern?

前端 未结 3 1372
自闭症患者
自闭症患者 2020-12-17 17:01

I\'m trying to remove a Service Locator from an abstract base class, but I\'m not sure what to replace it with. Here is a psuedo-example of what I\'ve got:

p         


        
相关标签:
3条回答
  • 2020-12-17 17:32

    I agree with @chrisichris and @Mark Seemann.

    Ditch the kernel from the controller. I'd switch your resolver composition a little bit so that your controller can remove the dependency on the IoC container and allow the resolver to be the only item that worries about the IoC container.

    Then I would let the resolver get passed into the constructor of the controller. This will allow your controller to be far more testable.

    For example:

    public interface IMyServiceResolver
    {
        List<IMyServiceInterface> Resolve(Type[] types);
    }
    
    public class NinjectMyServiceResolver : IMyServiceResolver
    {
        private IKernal container = null;
    
        public NinjectMyServiceResolver(IKernal container)
        {
            this.container = container;
        }
    
        public List<IMyServiceInterface> Resolve(Type[] types)
        {
            List<IMyServiceInterface> services = new List<IMyServiceInterface>();
    
            foreach(var type in types)
            {
                IMyServiceInterface instance = container.Get(type);
                services.Add(instance);
            }
    
            return services;
        }
    }
    
    public abstract class MyController : Controller
    {
        private IMyServiceResolver resolver = null;
    
        public MyController(IMyServiceResolver resolver) 
        { 
            this.resolver = resolver;
        }
    
        protected void DoActions(Type[] types)
        {
            var services = resolver.Resolve(types);
    
            foreach(var service in services)
            {
                service.DoAction();
            }
        }
    }
    

    Now your controller isn't coupled to a specific IoC container. Also your controller is much more testable since you can mock the resolvers and not require an IoC container at all for your tests.

    Alternatively, if you don't get to control when a controller is instantiated, you can modify it slightly:

    public abstract class MyController : Controller
    {
        private static IMyServiceResolver resolver = null;
    
        public static InitializeResolver(IMyServiceResolver resolver)
        {
            MyController.resolver = resolver;
        }
    
        public MyController() 
        { 
            // Now we support a default constructor
            // since maybe someone else is instantiating this type
            // that we don't control.
        }
    
        protected void DoActions(Type[] types)
        {
            var services = resolver.Resolve(types);
    
            foreach(var service in services)
            {
                service.DoAction();
            }
        }
    }
    

    You would then call this at your application start up to initialize the resolver:

    MyController.InitializeResolver(new NinjectMyServiceResolver(kernal));
    

    We did this to handle elements created in XAML who require dependencies resolved but we wanted to remove Service Locator like requests.

    Please excuse any syntactical errors :)

    I'm writing a blog post series on the topic of refactoring an MVVM application with Service Locator calls in the view models you might find interesting. Part 2 is coming soon :)

    http://kellabyte.com/2011/07/24/refactoring-to-improve-maintainability-and-blendability-using-ioc-part-1-view-models/

    0 讨论(0)
  • 2020-12-17 17:40

    I would have liked to have a bit more information before posting this answer, but Kelly put me on the spot. :) Telling me to put my code where my mouth is, so to speak.

    Like I said in my comment to Kelly, I disagree with moving the resolver/locator from a static implementation to an injected implementation. I agree with ChrisChris that the dependencies the derived type needs should be resolved in that class and not delegated to the base class.

    That said, here is how I would remove the service location...

    Create Command Interface

    First of all I would create a command interface for the specific implementation. In this case the types sent with the DoActions method are generated from attributes, so I would create an IAttributeCommand. I am adding a Matches method to the command in order to declare the command for use by certain types.

    public interface IAttributeCommand
    {
        bool Matches(Type type);
        void Execute();
    }
    

    Add Command Implementations

    To implement the interface, I pass in the specific dependencies I need to execute my command (to be resolved by my container). I add a predicate to my Matches method, and define my Execute behavior.

    public class MyTypeAttributeCommand : IAttributeCommand
    {
        MyDependency dependency;
                SomeOtherDependency otherDependency;
    
        public MyTypeAttributeCommand (MyDependency dependency, ISomeOtherDependency otherDependency)
        {
            this.dependency = dependency;
                        this.otherDependency = otherDependency
        }
    
        public bool Matches(Type type)
        {
            return type==typeof(MyType)
        }
        public void Execute()
        {
            // do action using dependency/dependencies
        }
    }
    

    Register Commands with Container

    In StructureMap (use your favorite container), I would register the array like so:

    Scan(s=>
           {
                    s.AssembliesFromApplicationBaseDirectory();
                    s.AddAllTypesOf<IAttributeCommand>();
                    s.WithDefaultConventions();
           } 
    

    Select and Execute Commands Based on Type

    Finally, on the base class, I define an IAttributeCommand array in my constructor arguments to be injected by the IOC container. When the derived type passes in the types array, I will execute the correct command based on the predicate.

    public abstract class MyController : Controller
    {
        protected IAttributeCommand[] commands;
    
        public MyController(IAttributeCommand[] commands) { this.commands = commands); }
    
        protected void DoActions(Type[] types)
        {
            foreach(var type in types)
            {
                var command = commands.FirstOrDefault(x=>x.Matches(type));
                if (command==null) continue;
    
                command.Execute();
            }
        }
    }
    

    If you multiple commands can handle one type, you can change the implementation: commands.Where(x=>x.Matches(type)).ToList().ForEach(Execute);

    The effect is the same, but there is a subtle difference in how the class is constructed. The class has no coupling to an IOC container and there is no service location. The implementation is more testable as the class can be constructed with its real dependencies, with no need to wire up a container/resolver.

    0 讨论(0)
  • 2020-12-17 17:41

    Maybe you should just do away the Kernel, Types and MySpecialResolver and let the subclasses call DoActions with the IMyServiceInterface instances they need as argument directly. And let the subclasses decide how they get to these instances - they should know best (or in case they don't know which exactly the one who ever decides which instances of IMyServiceInterface are needed)

    0 讨论(0)
提交回复
热议问题