Как реализовать клиентский код для абстрактной фабрики? - PullRequest
0 голосов
/ 09 января 2019

Мне трудно понять реализацию клиентского кода с помощью фабричного метода. Я понимаю общее использование абстрактных фабрик, но моя проблема в том, что я хочу, чтобы 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 ]

0 голосов
/ 09 января 2019

Как отмечается в комментариях, другой ответ является нарушением Принципа 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}'.");
    }
}

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

0 голосов
/ 09 января 2019

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

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

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

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

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