Как реализовать клиентский код для абстрактной фабрики?

Мне трудно понять реализацию клиентского кода с помощью фабричного метода. Я понимаю общее использование абстрактных фабрик, но моя проблема в том, что я хочу, чтобы Factory выясняла правильный объект для создания экземпляра во время выполнения, но каждая реализация, которую я вижу, включает передачу перечисления или какого-либо другого значения конструктору.

Это мой текущий дизайн

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>();
    }
}

Проблема в том, что я пытаюсь заставить фабрику создать правильный провайдер на основе данных, которые пользователь заполняет в веб-форме. При нажатии create я хочу, чтобы на фабрике был создан правильный провайдер и запущена правильная логика. Но с этой реализацией я вынужден сделать что-то вроде

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

Когда я действительно хочу иметь возможность получить тип Exchange от пользователя во время выполнения из веб-формы и передать его на завод

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

Это возможно? Мне интересно (или правильное решение), или я на правильном пути, но определенно препятствует разрыв в знаниях.

2 ответа

Решение

Как отмечается в комментариях, другой ответ является нарушением принципа O/C (и немного принципа единой ответственности (SRP)) SOLID.

Более динамичный подход - внедрить все экземпляры обмена и выбрать правильный. Ниже приведен пример, основанный на имени класса (не полное имя, но его можно легко изменить).

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;
    }
}

Его можно легко расширить, зарегистрировав дальнейшую реализацию с помощью DI, без каких-либо изменений кода на заводе.

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

В сценариях с высокой производительностью (пара из 1000 запросов в секунду), когда внедренные сервисы находятся в области действия / переходных процессах или нагрузка памяти / ГХ при создании этих дополнительных экземпляров высока, вы можете использовать шаблон провайдера только для создания действительно необходимого обмена:

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}'.");
    }
}

Этот подход аналогичен первому, за исключением того, что вы расширяете его, добавляя новые IExchangeProviders. Оба подхода позволяют расширять обмены без изменений на ExchangeFactory (или в сценариях с высокой производительностью LazyExchangeFactory)

Как правило, вы не должны указывать фабрике, какой конкретный тип создать. Вы должны предоставить ему информацию, необходимую для принятия этого решения. Я не говорю, что это не может быть соотношение 1:1, просто вызывающая сторона не должна указывать фабрике, чтобы она создавала конкретный конкретный тип.

Представь, что у тебя есть Student объект с Grade имущество. У вас также есть завод, который производит ISchoolи конкретные реализации ElementarySchool, MiddleSchool, а также HighSchool, Теперь у вас может быть 3 метода: CreateElementarySchool(), CreateMiddleSchool() а также CreateHighSchool(), но тогда звонящий должен решить, какой он хочет.

Лучше всего иметь метод, который использует некоторую информацию для создания школы. Например: CreateSchoolForGrade(grade), Внутренне, у фабрики будет логика, которая определяет, какой тип бетона соответствует марке.

В вашем случае, если у вас есть набор из 2 типов для выбора на веб-форме, вы можете принять тип (скажем, варианты: Empire или Rebels). Вы могли бы иметь перечисление:

public enum Faction
{
    Empire,
    Rebels
}

а затем заводской метод:

public IFaction CreateFaction(Faction faction)
{
    switch (faction)
    {
        case Faction.Empire:
            return new EmpireFaction();
        case Faction.Rebels:
            return new RebelsFaction();
        default:
            throw new NotImplementedException();
    }
}

Теперь представьте, что вы удалили EmpireFaction, заменив ее на EmpireFactionV2. Вам нужно только изменить свою фабрику, а вызывающему абоненту все равно:

public IFaction CreateFaction(Faction faction)
{
    switch (faction)
    {
        case Faction.Empire:
            return new EmpireFactionV2();
        case Faction.Rebels:
            return new RebelsFaction();
        default:
            throw new NotImplementedException();
    }
}
Другие вопросы по тегам