How to implement an [GoF]-ish Abstract Factory Pattern using an IoC like Ninject
Asked Answered
N

1

2

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

Natoshanatron answered 6/1, 2014 at 16:27 Comment(5)
Do you really need the factory at all? Why don't you just inject the product into a consumer if it's needed?Tyrannous
Do the factories need some other configuration dependent implementation other than "selecting" the correct specific product type (i.E. FactoryA => Product1A, FactoryB => Product1B)? .Arie
Does configuration A need the same Factory instance twice (configuration.factory == configuration.component.factory) or is it enough when they are of the same type?Arie
@Tyrannous : In this example the Factory is not really "needed" to do a heavy task but that is not the point. The point is that there is a unit test which defines how the factory should look like. This on the other hand implies that it is needed to make the code compile and run as expected.Natoshanatron
@Arie I will add some clarifications to your questions and edit the given example explanation a bit.Natoshanatron
A
3

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.

Arie answered 7/1, 2014 at 9:46 Comment(3)
@BatterBackupUnit Thank you very much, this solution is gorgeous and is exactly that kind of solution i was looking for. I think the missing bits were how to put in the runtime parameter correctly paired with the binding. That would have taken me a while to figure out, indeed! FYI: I would like to vote you up for the use of FluentAssertions as i wasn't familiar with this, but i can't because i need 15P of reputation to do so. hehe Thanks again GreetingsNatoshanatron
@Arie quick question. If I remove the .DefinesNamedScope(ConfigurationScopeName) and .InNamedScope(ConfigurationScopeName) I get an error "Expected object to be Castle.Proxies.IFactoryProxy{}, but found Castle.Proxies.IFactoryProxy{}." at configA.component.factory.Should().Be(configA.factory);. Any idea why?Insentient
@kooshka: it's a different instance of the same factory. If you don't need there to be a "singleton per configuration" of the factory, you can get rid of the named scope stuff. It's expected then for the verification configA.component.factory.Should().Be(configA.factory) to fail, but the factory will still produce an A "product" and not a B product.Arie

© 2022 - 2024 — McMap. All rights reserved.