How to use a factory with Dependecy Injection without resorting to using Service Locator pattern

后端 未结 3 1831
故里飘歌
故里飘歌 2021-02-06 18:17

I have a GUI application. In it I allow a user to select from a container-provided list of algorithms. Each algorithm will be kicked off as a background task in another view.

3条回答
  •  野的像风
    2021-02-06 19:10

    I really appreciate everyone else's answers. They're going to help me get this problem solved. I will most likely accept Remo's answer since it matches the current problem I actually face.

    For my understanding I'd also like to get feedback on this broader answer I've come up with.


    I was not sure Dependency Injection directly supported the mechanisms I'd talked about, through constructor, property, or method injection. These are what I'd consider "pure" DI at this point - though I'm willing to be swayed.

    I thought injecting dependencies meant a relatively static object graph. It could be loaded from a config file, or procedurally generated, but it couldn't directly accommodate an unknowable runtime state, like a user repeatedly requesting new instances.

    However after thinking through some of these alternatives I'm starting to think that there are work-arounds that support the purity, and that maybe the purity I described isn't as important as I'd thought. Some of the less "pure" options still work with most containers in a mostly clean manner, and it seems easy enough to add support to the container to clean them up the rest of the way.

    Here are the work-arounds I've considered so far (a few of these were already mentioned).

    Reference the container in custom factories, then wash your hands:

    Your components can be implemented however you want. You can use whatever container you want (as long as it supports transient instances). You just have to live with the fact that you'll be injecting factories into your code, and that those factories will resolve from the container directly. Who needs purity when you can be pragmatic?

    Example code:

    public class ComponentFactory // Might inherit from an interface...
    {
        private readonly IContainer container;
    
        public ComponentFactory(IContainer container)
        {
            this.container = container;
        }
    
        public IComponent Create(IOtherComponent otherComponent)
        {
            return container.Get(otherComponent);
        }
    }
    

    Use a container-specific factory extension:

    Your components can be implemented however you want. But your container must directly support injecting factories into your code, and auto-implementing those factories so they don't have to have any specific knowledge of the container.

    Example code:

    // Black magic - the container implemented it for us!
    // But the container basically implemented our code from the previous example...
    public interface IComponentFactory
    {
        public IComponent Create(IOtherComponent otherComponent);
    }
    

    Use a container-specific object pool:

    Make sure your components are stateless, and allow them to be pooled. The container will take care of allocating or pooling objects for you. This is more or less a managed factory with a fancy implementation, that you have to both allocate from and deallocate to.

    Pseudo-code (I haven't used a conatiner-based object pool before):

    public class SomeUI
    {
        private readonly IComponentPool componentPool;
    
        public OtherComponent(IComponentPool componentPool)
        {
            this.componentPool = componentPool;
        }
    
        public void DoSomethingWhenButtonPushed()
        {
            var component = componentPool.Get();
            component.DoSomething();
            componentPool.Release(component);
        }
    }
    

    The advantage of this pseudo-code is that you didn't have to define the interface for your factory. The disadvantage is that you had to depend on the pool interface, so your container has its tendrils on you. Also I didn't get to pass in anything to the Get method. This probably makes sense because the objects have to support instance reuse.

    If real pools don't work like this, they might look identical to the "container-specific factory extension" example above, only they'd always require a Release method along with the Create method.

    Use the Flyweight Pattern:

    (Flyweight Pattern - Not sure if I've identified the pattern correctly, or if I just have a weird use for it)

    Inject a stateless component that acts as the behavior for your objects, or the "heavy-weight" component. Support separate "instances" of the component using flyweight state objects that you pass to the behavior component, or have them wrap the behavior component.

    This will drastically effect the architecture of your components. Your implementations must be stateless, and your state object must be designed in a way that it will work for all possible component implementations. But it completely supports the "pure" injection model (only injection into constructors, properties, methods).

    This won't work well for UIs though. View classes usually need to be created directly, and we don't want our "flyweight" to be a UI class...

    public class ComponentState
    {
        // Hopefully can be less generic than this...
        public Dictionary Data { get; set; }
    }
    
    public interface IComponent
    {
        int DoSomething(ComponentState state);
    }
    
    public SomeUI
    {
        private readonly IComponent component;
    
        public OtherComponent(IComponent component)
        {
            this.component = component;
        }
    
        public void DoSomethingWhenButtonPushed()
        {
            var state = new ComponentState();
            component.DoSomething(state);
        }
    }
    

    Use child containers for each new instance the user requests:

    The container works best when creating an object graph from a single root. Instead of trying to fight this, work with it. When the user clicks a button to create a new instance of your algorithm, create a new child container for those objects and call into global configuration code however it needs to be done. Then attach the child container to the parent container.

    This means the spawning code will need to know about the container at some level. Maybe wrapping it in a factory is the best way.

    public class SubComponentFactory // Might inherit from an interface...
    {
        private readonly IContainer container;
    
        public ComponentFactory(IContainer container)
        {
            this.container = container;
        }
    
        public IComponent Create(IOtherComponent otherComponent)
        {
            // Todo: Figure out any lifecycle issues with this.
            // I assume the child containers get disposed with the parent container...
    
            var childContainer = container.CreateChildContainer();
            childContainer.Configure(new SubComponentConfiguration());
    
            return childContainer.Get(otherComponent);
        }
    }
    

    Kind of looks like where we started. But we have a new object graph root, so I'm not sure I can call this out for using the Service Locator pattern. The problem with this approach is the most tightly coupled to the container. Not only is the container directly referenced, but the factory relies on the implementation detail of the container supporting child containers.

提交回复
热议问题