Abstract
When the design requires an "Abstract Factory Pattern" like stated by the [GoF] including several products and over some product families, then setting up an IoC can become a bit tricky. Especially when the specific factory implementations need to be dispatched by runtime parameters and shared among some subsequent components.
Given the follwing API, i was trying to set up my IoC (Ninject in this case) to retrieve Configuration
objects configured through a IConfigurationFactory
. The configuration stores an IFactory
instance whoose implementation is determined by a runtime parameter of type ProductFamily
. Afterwards the product types created by the factory inside of the configuration should always match the requested ProductFamily
. The subgraph, consisting of the Component
class holds the same IFactory
per Configuration
.
public enum ProductFamily { A, B }
public interface IProduct1 { }
public interface IProduct2 { }
public interface IFactory
{
IProduct1 CreateProduct1();
IProduct2 CreateProduct2();
}
public class Configuration
{
public readonly IFactory factory;
public readonly Component component;
public Configuration(IFactory factory, Component component)
{
this.factory = factory;
this.component = component;
}
}
public class Component
{
public IFactory factory;
public Component(IFactory factory) { this.factory = factory; }
}
public interface IConfigurationFactory
{
Configuration CreateConfiguration(ProductFamily family);
}
Tests
To clarify the intended behaviour i have added my test code writte in vstest. But forehand some additions, thanks to @BatterBackupUnit for asking these nitty details:
- The factories do only need the
ProductFamily
as a parameter to choose between the implementations, nothing else
- Every
Configuration
and its subsequent objects like the Component
, share the same factory instance
So i hope this helps :)
[TestMethod]
public void TestMethod1()
{
var configFac = ComposeConfigurationFactory();
// create runtime dependent configs
var configA = configFac.CreateConfiguration(ProductFamily.A);
var configB = configFac.CreateConfiguration(ProductFamily.B);
// check the configuration of the factories
Assert.IsInstanceOfType(configA.factory.CreateProduct1(), typeof(Product1A));
Assert.IsInstanceOfType(configB.factory.CreateProduct1(), typeof(Product1B));
Assert.IsInstanceOfType(configA.factory.CreateProduct2(), typeof(Product2A));
Assert.IsInstanceOfType(configB.factory.CreateProduct2(), typeof(Product2B));
// all possible children of the configuration should share the same factory
Assert.IsTrue(configA.factory == configA.component.factory);
// different configurations should never share the same factory
var configA2 = configFac.CreateConfiguration(ProductFamily.A);
Assert.IsTrue(configA.factory != configA2.factory);
}
This qestion has already been solved therefore i removed all the unnecessary fluff.
Thanks to @BatteryBackupUnit for your time and effort
Best regards
Isaias
The following alternative passes all your tests while remaining fairly generic.
The bindings define all configuration dependencies. The only non-binding code which is ninject specific is the IConfigurationFactory which puts the necessary configuration information (=>ProductFamily) on the ninject context.
You will need the following nuget packages to make this code compile:
- Fluent Assertions
- Ninject
- Ninject.Extensions.ContextPreservation
- Ninject.Extensions.Factory
- Ninject.Extensions.NamedScope
Here's the code:
using System.Linq;
using FluentAssertions;
using Ninject;
using Ninject.Activation;
using Ninject.Extensions.Factory;
using Ninject.Extensions.NamedScope;
using Ninject.Modules;
using Ninject.Parameters;
using Ninject.Planning.Targets;
using Ninject.Syntax;
public class Program
{
private static void Main(string[] args)
{
var kernel = new StandardKernel();
kernel.Load<AbstractFactoryModule>();
var configFac = kernel.Get<ConfigurationFactory>();
// create runtime dependent configs
var configA = configFac.CreateConfiguration(ProductFamily.A);
var configB = configFac.CreateConfiguration(ProductFamily.B);
configA.factory.CreateProduct1().Should().BeOfType<Product1A>();
configB.factory.CreateProduct1().Should().BeOfType<Product1B>();
configA.component.factory.Should().Be(configA.factory);
configA.factory.Should().NotBe(configB.factory);
}
}
public enum ProductFamily { A, B }
public interface IProduct1 { }
public interface IFactory
{
IProduct1 CreateProduct1();
}
public class Product1A : IProduct1 { }
public class Product1B : IProduct1 { }
public class Configuration
{
public readonly IFactory factory;
public readonly Component component;
public Configuration(IFactory factory, Component component)
{
this.factory = factory;
this.component = component;
}
}
public class Component
{
public IFactory factory;
public Component(IFactory factory) { this.factory = factory; }
}
public interface IConfigurationFactory
{
Configuration CreateConfiguration(ProductFamily family);
}
public class ConfigurationFactory : IConfigurationFactory
{
private readonly IResolutionRoot resolutionRoot;
public ConfigurationFactory(IResolutionRoot resolutionRoot)
{
this.resolutionRoot = resolutionRoot;
}
public Configuration CreateConfiguration(ProductFamily family)
{
return this.resolutionRoot.Get<Configuration>(new AbstractFactoryConfigurationParameter(family));
}
}
public class AbstractFactoryConfigurationParameter : IParameter
{
private readonly ProductFamily parameterValue;
public AbstractFactoryConfigurationParameter(ProductFamily parameterValue)
{
this.parameterValue = parameterValue;
}
public ProductFamily ProductFamily
{
get { return this.parameterValue; }
}
public string Name
{
get { return this.GetType().Name; }
}
public bool ShouldInherit
{
get { return true; }
}
public object GetValue(IContext context, ITarget target)
{
return this.parameterValue;
}
public bool Equals(IParameter other)
{
return this.GetType() == other.GetType();
}
}
public class AbstractFactoryModule : NinjectModule
{
private const string ConfigurationScopeName = "ConfigurationScope";
public override void Load()
{
this.Bind<IConfigurationFactory>().To<ConfigurationFactory>();
this.Bind<Configuration>().ToSelf()
.DefinesNamedScope(ConfigurationScopeName);
this.Bind<IFactory>().ToFactory()
.InNamedScope(ConfigurationScopeName);
this.Bind<IProduct1>().To<Product1A>()
.WhenProductFamiliy(ProductFamily.A);
this.Bind<IProduct1>().To<Product1B>()
.WhenProductFamiliy(ProductFamily.B);
}
}
public static class AbstractFactoryBindingExtensions
{
public static IBindingInNamedWithOrOnSyntax<T> WhenProductFamiliy<T>(this IBindingWhenInNamedWithOrOnSyntax<T> binding, ProductFamily productFamily)
{
return binding
.When(x => x.Parameters.OfType<AbstractFactoryConfigurationParameter>().Single().ProductFamily == productFamily);
}
}
Please note that I am not convinced that the Named Scope is necessary for your use case. The named scope ensures that there is only one instance of a type (here: the IFactory) per scope (here: the configuration instance). So you basically get an "IFactory
singleton per configuration".
In the above example code it certainly is not required as the factory instances are not specific to configuration. If the factories are specific to a configuration create a binding for each and also use the .WhenProductFamily(..)
binding extension to make sure the correct factory gets injected.
Also note that you can make the AbstractFactoryConfigurationParameter
and the .WhenProductFamily(..)
extension more generic so you can reuse it for multiple different abstract factories.