Search code examples
c#model-view-controllerasp.net-corefactoryabstract-factory

How to implement the client code for an Abstract Factory?


I'm having a hard time understanding the implementation of client code with the factory method. I understand the overall use of Abstract Factories but my issue is I want the Factory to figure out the correct object to instantiate at runtime, but every implementation I see involves passing an enum or some other value to the constructor.

This is my current design

using System;

namespace FactoryTest.Jobs
{
    public class ExchangeProvider1 : IExchangeProvider
    {
        public void Buy()
        {
            Console.WriteLine("Buying on Exchange1!");
        }
    }
}

using System;

namespace FactoryTest.Jobs
{
    public class ExchangeProvider2 : IExchangeProvider
    {
        public void Buy()
        {
            Console.WriteLine("Buying on Exchange2");
        }
    }
}

  public interface IExchangeFactory
{

}

   public interface IExchangeProvider
{
    void Buy();
}

  public class ExchangeFactory : IExchangeFactory
{
    public static IExchangeProvider CreateExchange<T>() where T : IExchangeProvider
    {
        return Activator.CreateInstance<T>();
    }

    public static IExchangeProvider CreateExchange(string exchangeName)
    {
        return (IExchangeProvider) Activator.CreateInstance<IExchangeProvider>();
    }
}

The problem is that I'm trying to have the factory build the correct provider based on details the user fills out in a web form. On hitting create I want to the factory to instantiate the correct provider and run the correct logic. But with this implementation Im forced to do something like

var provider = ExchangeFactory.CreateExchange<Exchange1>();

When I really want to be able to get the Exchange Type from the user at runtime from the web form and pass it to the factory

//Receive IExchangeType from user submitting web form
var provider = ExchangeFactory.CreateExchange<IExchangeType>();

Is this possible? I'm wondering (or the correct solution), or if I'm on the right track but am definitely hindered by a gap in knowledge.


Solution

  • As noted in the comments the other answer is a violation of O/C Principle (and a bit of Single Responsibility Principle (SRP)) of SOLID.

    A more dynamic approach is to inject all instances of the exchange and pick the correct one. Bellow example is based on the class name (not full-qualifed name, but that cane easily be changed).

    public interface IExchange
    {
        void Buy();
    }
    
    public class Exchange1 : IExchange
    {
        public void Buy() => Console.WriteLine("Buying on Exchange1");
    }
    
    public class Exchange2 : IExchange
    {
        public void Buy() => Console.WriteLine("Buying on Exchange2");
    }
    
    public interface IExchangeFactory
    {
        IExchange CreateExchange(string exchangeName);
    }
    
    // All exchanges are instantiated and injected
    public class ExchangeFactory : IExchangeFactory
    {
        private readonly IEnumerable<IExchange> exchanges;
    
        public ExchangeFactory(IEnumerable<IExchange> exchanges)
        {
            this.exchanges = exchanges ?? throw new ArgumentNullException(nameof(exchanges));
        }
    
        public IExchange CreateExchange(string exchangeName)
        {
            var exchange = exchanges.FirstOrDefault(e => e.GetType().Name == exchangeName);
            if(exchange==null)
                throw new ArgumentException($"No Exchange found for '{exchangeName}'.");
    
            return exchange;
        }
    }
    

    It can easily be extended by registering further implementation with the DI, w/o any code changes on the factory

    service.AddScoped<IExchange, Exchange3>();
    service.AddScoped<IExchange, Exchange4>();
    

    In high performance scenarios (a couple of 1000 requests per second) where the injected services are scoped/transient or the memory/GC pressure on creating this extra instances is high, you can use the provider pattern to only create the exchange that's really required:

    public interface IExchangeProvider
    {
        IExchange CreateExchange(string exchangeName);
    }
    
    public class Exchange1Provider : IExchangeProvider
    {
        public IExchange CreateExchange(string exchangeName)
        {
            if(exchangeName == nameof(Exchange1))
            {
                // new it, resolve it from DI, use activation whatever suits your need
                return new Exchange1();
            }
    
            return null;
        }
    }
    
    public class Exchange2Provider : IExchangeProvider
    {
        public IExchange CreateExchange(string exchangeName)
        {
            if (exchangeName == nameof(Exchange2))
            {
                // new it, resolve it from DI, use activation whatever suits your need
                return new Exchange1();
            }
    
            return null;
        }
    }
    
    public class LazyExchangeFactory : IExchangeFactory
    {
        private readonly IEnumerable<IExchangeProvider> exchangeProviders;
    
        public LazyExchangeFactory(IEnumerable<IExchangeProvider> exchangeProviders)
        {
            this.exchangeProviders = exchangeProviders ?? throw new ArgumentNullException(nameof(exchangeProviders));
        }
    
        public IExchange CreateExchange(string exchangeName)
        {
            // This approach is lazy. The providers could be singletons etc. (avoids allocations)
            // and new instance will only be created if the parameters are matching
            foreach (IExchangeProvider provider in exchangeProviders)
            {
                IExchange exchange = provider.CreateExchange(exchangeName);
    
                // if the provider couldn't find a matcing exchange, try next provider
                if (exchange != null)
                {
                    return exchange;
                }
            }
    
            throw new ArgumentException($"No Exchange found for '{exchangeName}'.");
        }
    }
    

    This approach is similar to the first, with the exception that you are extending it by adding new IExchangeProviders. Both approaches allow you to extend the exchanges w/o a change on ExchangeFactory (or in high performance scenarios LazyExchangeFactory)