Search code examples
c#autofacstrategy-patternfactory-method

Strategy & factory pattern for base/descendant class resolution


I'm refactoring a codebase and stumbled upon a factory class that created objects based on the subtype passed into the method.

The class basically has one public method with one parameter of which it is a descendant from a base class. Within this method is a switch statement that determines which subtype is passed and conditionally calls different methods to produce the result.

I'm trying to tidy up a bit and figured a strategy pattern might suit the requirements since the code violates the open-closed principle.

Since Autofac is being used, I figured that the transition would be straight forward, however I've hit a bump in the road.

The problem isn't related to Autofac, but rather to the choice of design.

The following code illustrates the class composition, but it is lacking.

public abstract class Parent { }
public class ChildA : Parent { }
public class ChildB : Parent { }

public interface IChildStrategy<T> where T:Parent
{
    IEnumerable<object> CreateObjects(Parent child);
}

public class ChildAStrategy : IChildStrategy<ChildA>
{
    private IEnumerable<object> CreateObjects(ChildA child)
    {
        yield return "child A";
    }

    public IEnumerable<object> CreateObjects(Parent child) => 
        CreateObjects(child as ChildA);
}

public class ChildBStrategy : IChildStrategy<ChildB>
{
    private IEnumerable<object> CreateObjects(ChildB child)
    {
        yield return "child b";
        yield return "child b's pet";
    }

    public IEnumerable<object> CreateObjects(Parent child) =>
        CreateObjects(child as ChildB);         
}

[TestMethod]
public void TestStrategyPattern()
{
    var container = builder.Build();

    Parent child = new ChildA();
    var type = child.GetType();
    var strategy = container.Resolve(typeof(IChildStrategy<>)
        .MakeGenericType(type));

    // strategy.CreateObjects(child);
    // Assert.AreEqual("child A", fromDict);

    var dict = new Dictionary<Type, Func<Parent, IEnumerable<object>>>();

    dict.Add(typeof(ChildA), x => new ChildAStrategy().CreateObjects(x));
    dict.Add(typeof(ChildB), x => new ChildBStrategy().CreateObjects(x));

    var fromDict = dict[type](child);

    Assert.AreEqual("child A", fromDict);
}

I've tried both registering the interface with the generic type itself, like so:

public interface IChildStrategy<T> where T:Parent
{
    IEnumerable<object> CreateObjects(T child);
}

But it doesn't really change the difficulties.

Are there any good alternatives to a design pattern for sub-classes?

Updated

Here's what I ended up with. The changes are basically removing the parameter from the CreateObjects method and rather inject it into the constructor as a dependency and registering the strategies as Keyed<T> registrations.

public abstract class Parent { }
public class ChildA : Parent { }
public class ChildB : Parent { }

public interface IChildStrategy
{
    IEnumerable<object> CreateObjects();
}

public class ChildAStrategy : IChildStrategy
{
    private readonly ChildA childA;

    public ChildAStrategy(ChildA childA)
    {
        this.childA = childA;
    }

    public IEnumerable<object> CreateObjects()
    {
        yield return childA;
    }
}

public class ChildBStrategy : IChildStrategy
{
    private readonly ChildB childB;

    public ChildBStrategy(ChildB childB)
    {
        this.childB = childB;
    }

    public IEnumerable<object> CreateObjects()
    {
        yield return childB;
        yield return "child b's pet";
    }
}

[TestMethod]
public void TestStrategyPattern()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<ChildAStrategy>().Keyed<IChildStrategy>(typeof(ChildA));
    builder.RegisterType<ChildBStrategy>().Keyed<IChildStrategy>(typeof(ChildB));

    var container = builder.Build();

    IChildStrategy resolve(Parent x) => container.ResolveKeyed<IChildStrategy>(x.GetType(), new TypedParameter(x.GetType(), x));

    Parent root;
    IChildStrategy strategy;
    root = new ChildA();
    strategy = resolve(root);
    Assert.IsInstanceOfType(strategy, typeof(ChildAStrategy));
    Assert.AreSame(root, strategy.CreateObjects().Single());
    root = new ChildB();
    strategy = resolve(root);
    Assert.IsInstanceOfType(strategy, typeof(ChildBStrategy));
    Assert.AreSame(root, strategy.CreateObjects().First());
    Assert.AreEqual("child b's pet", strategy.CreateObjects().Last());
}

Solution

  • Updated Answer

    Original answer is included, below

    I think the pattern you're looking for is a Mediator.

    Here's an example implementation that fits your needs, as I understand them. This implementation strengthens the typing of the handlers, but makes even heavier use of dynamic in the mediator itself. If performance is a concern, you might want to run some tests--this implementation will probably be slower than your existing code (see: How does having a dynamic variable affect performance?)

    using System.Collections.Generic;
    using System.Linq;
    using Autofac;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace _54542354.MediatorExample
    {
        /**
         * Example Input/Output types
         **/
        abstract class ActionBase { }
        class ExampleAction : ActionBase { public string Name { get; set; } }
        class ReturnType { public string Id { get; set; } }
    
        /**
         * Interfaces
         **/
        interface IActionHandler<TAction> where TAction : ActionBase
        {
            IEnumerable<ReturnType> Handle(TAction action);
        }
    
        interface IActionHandlerMediator
        {
            IEnumerable<ReturnType> Handle(ActionBase action);
        }
    
        /**
         * Example implementations
         **/
        class ExampleHandler : IActionHandler<ExampleAction>
        {
            public IEnumerable<ReturnType> Handle(ExampleAction action)
            {
                yield return new ReturnType{ Id = $"{action.Name}_First" };
                yield return new ReturnType{ Id = $"{action.Name}_Second" };
            }
        }
    
        class ActionHandlerMediator : IActionHandlerMediator
        {
            readonly ILifetimeScope container;
    
            public ActionHandlerMediator(ILifetimeScope container)
                => this.container = container;
    
            public IEnumerable<ReturnType> Handle(ActionBase action)
            {
                // TODO: Error handling. What if no strategy is registered for the provided type?
                dynamic handler = container.Resolve(typeof(IActionHandler<>)
                    .MakeGenericType(action.GetType()));
    
                return (IEnumerable<ReturnType>)handler.Handle((dynamic)action);
            }
        }
    
        /**
         * Usage example
         **/
        [TestClass]
        public class Tests
        {
            [TestMethod]
            public void TestMediator()
            {
                var builder = new ContainerBuilder();
                builder.RegisterType<ExampleHandler>().As<IActionHandler<ExampleAction>>();
                builder.RegisterType<ActionHandlerMediator>().As<IActionHandlerMediator>();
                var container = builder.Build();
    
                var handler = container.Resolve<IActionHandlerMediator>();
    
                var result = handler.Handle(new ExampleAction() { Name = "MyName" });
    
                Assert.AreEqual("MyName_First", result.First().Id);
                Assert.AreEqual("MyName_Second", result.Last().Id);
            }
        }
    }
    

    Original Answer

    I took a stab at running your sample code. I had to tweak some things out of the box, but I think it actually worked as you want it to (after my tweaks).

    Here's what I ended up with:

    [TestMethod]
    public void TestStrategyPattern_Dict()
    {
        // Define the available strategies
        var dict = new Dictionary<Type, Func<Parent, IEnumerable<object>>>();
        dict.Add(typeof(ChildA), x => new ChildAStrategy().CreateObjects(x));
        dict.Add(typeof(ChildB), x => new ChildBStrategy().CreateObjects(x));
    
        // Create the input object
        Parent child = new ChildA();
    
        // Invoke the strategy
        IEnumerable<object> enumerable = dict[child.GetType()](child);
    
        // Verify the results
        Assert.AreEqual("child A", enumerable.Single());
    }
    
    [TestMethod]
    public void TestStrategyPattern_AutoFac()
    {
        // Define the available strategies
        var builder = new ContainerBuilder();
        builder.RegisterType<ChildAStrategy>().As<IChildStrategy<ChildA>>();
        builder.RegisterType<ChildBStrategy>().As<IChildStrategy<ChildB>>();
        var container = builder.Build();
    
        // Create the input object
        Parent child = new ChildA();
    
        // Resolve the strategy
        // Because we don't know exactly what type the container will return,
        // we need to use `dynamic`
        dynamic strategy = container.Resolve(typeof(IChildStrategy<>)
            .MakeGenericType(child.GetType()));
    
        // Invoke the strategy
        IEnumerable<object> enumerable = strategy.CreateObjects(child);
    
        // Verify the results
        Assert.AreEqual("child A", enumerable.Single());
    }
    

    These tests both pass. I did not change any code outside of the tests.

    The two main changes I introduced are:

    • Use of .Single() before asserting. This is necessary because the strategy returns an IEnumerable, but the assertion is expecting the first object from that enumerable.
    • Use of the dynamic type when resolving the strategy from AutoFac. This is necessary because the compiler can't tell what type AutoFac will return. In the original code, the returned type was object, which doesn't have a CreateObjects(Parent) method. dynamic lets us call any method we want--the compiler will just assume it exists. We'll get a runtime exception if the method doesn't exist, but because we know we just created an IChildStrategy<>, we can be confident that the method will exist.