Как реализовать клиентский код для абстрактной фабрики?
Мне трудно понять реализацию клиентского кода с помощью фабричного метода. Я понимаю общее использование абстрактных фабрик, но моя проблема в том, что я хочу, чтобы 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}'.");
}
}
Этот подход аналогичен первому, за исключением того, что вы расширяете его, добавляя новые IExchangeProvider
s. Оба подхода позволяют расширять обмены без изменений на 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();
}
}