質問

I'm using Ninject 3.0.1.10 and ninject.extensions.factory 3.0.1.0 from NuGet - in the 'real' scenario I'll be using ninject.extensions.conventions as well (rather than manually binding IFoo), but I wanted to keep that out of this to try and simplify the question.

I have an IFoo interface and multiple implementations of it, with each under a child namespace and subfolder called Gen1 and Gen2. I have an IFooFactory interface where the intent is that it returns an IFoo based on a specified parameter (string, enum, whatever).

I use an enum in this example simply to try and make it more clear - I had made a string version at first, but felt like the objections around passing a more arbitrary parameter like a string would just confuse the issue.

public enum ImplementationGeneration
{
    Gen1,
    Gen2,
    Gen3,
}

public interface IFoo
{
    void DoStuff();
}

public interface IFooFactory
{
    IFoo CreateFoo(ImplementationGeneration implementationGeneration);
}


namespace SomeRootNamespace.Gen1
{
    public class Foo : IFoo
    {
        public void DoStuff()
        {
            Console.WriteLine("Doing Type1 stuff");
        }
    }
}

namespace SomeRootNamespace.Gen2
{
    public class Foo : IFoo
    {
        public void DoStuff()
        {
            Console.WriteLine("Doing Type2 stuff");
        }
    }
}

Now, I understand that having the consumer 'choose' the implementation like this is a form of coupling that would ideally not exist, but IMHO it's the same level of coupling as named bindings, which Ninject already supports. I wanted to avoid adding the attributes to the implementations, and having GetGen1 / GetGen2 / etc methods in the factory interface is a painful fit for this, since I'd end up violating OCP via a switch somewhere to map input to the method to call (or manually using reflection)

The complete/working code I have now that I'd rather avoid if possible is here: https://gist.github.com/4549677

It uses 2 approaches:

  1. a manual factory implementation that violates OCP with a switch on the enum passed
  2. using the factory extension with an instance of IInstanceProvider (subclasses StandardInstanceProvider to override GetInstance).

The second approach seems like it might be 'close' to the 'right way' to get this working, but 1) it keeps around a reference to the kernel in order to do its work, which is probably a bad idea and 2) since I couldn't find the concrete type in the bindings for IFoo when searching on all the IFoo bindings during the call, it currently does a GetAll, so it instantiates N-1 more instances than it needs to for this scenario.

役に立ちましたか?

解決

Well, I found something at least better than what I had in the above.

It uses named bindings after all, with ninject.extensions.conventions being used to name all bindings based on the last part of their namespace. This ends up attaching names to lots of bindings that don't need it (ones with only a single implementation available for the given interface), although attaching names to those bindings doesn't cause any problems in their use (at least in my testing).

If for some reason that were an issue for whoever runs across this in the future, you could just be more specific in the code that sets up the binding via conventions - for instance, only adding the named binding if the last namespace part were in a particular set or matched a particular pattern.

kernel.Bind(x => x
    .FromThisAssembly()
    .SelectAllClasses()
    .BindAllInterfaces()
    .Configure((binding, concreteType) =>
    {
        var concreteNamespace = concreteType.Namespace ?? String.Empty;
        var lastNamespacePart = concreteNamespace.Split('.').Last();
        binding.Named(lastNamespacePart);
    })
);

It then uses the UseFirstArgumentAsNameInstanceProvider to look up the binding name based on the first parameter to the factory method (so you don't have to have separate methods for GetGen1, GetGen2, etc). I just changed the GetName override to do ToString since I was passing an enum instead of an actual string, but otherwise it's the same as from the linked wiki page.

public class UseFirstArgumentAsNameInstanceProvider : StandardInstanceProvider
{
    protected override string GetName(System.Reflection.MethodInfo methodInfo, object[] arguments)
    {
        return arguments[0].ToString();
    }

    protected override ConstructorArgument[] GetConstructorArguments(System.Reflection.MethodInfo methodInfo, object[] arguments)
    {
        return base.GetConstructorArguments(methodInfo, arguments).Skip(1).ToArray();
    }
}

I'm going to leave the question open for awhile just in case there's a different/better option available, but at least this seems reasonable and doesn't have any obvious OCP problems. :)

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top