Фабрика на основе входной переменной

Я прочитал несколько руководств о фабрике, абстрактном фабричном шаблоне и видел несколько примеров. В одном из руководств я прочитал, что фабричный шаблон может заменить основные операторы «если» или «переключить регистр» и следует принципам открытости/закрытости (твердых).

В одном из моих проектов есть огромный «корпус переключателей», который я хочу заменить (n) (абстрактной) фабрикой. Он уже основан на интерфейсе, поэтому реализация фабрики не должна быть такой сложной, но во всех примерах, которые я читал в учебниках, фабрика создавала один конкретный тип на основе конфигурации. Может ли кто-нибудь указать мне правильное направление, как реализовать фабрику, которая могла бы производить несколько типов на основе перечисления, которое следует принципам Solid и заменяет большой «случай переключателя»... или я дезинформирован и является «случай переключателя» переехали на завод?

код на данный момент:

public interface ISingleMailProcessor : IMailProcessor
{
    MailResult ProcesMail(Mail mail);
}


public MailResult GetMailResult(mail)
{
  ISingleMailProcessor mailprocessor;
  switch (mail.MailType)
  {
      case MailConnector.MailType.AcronisBackup:
         mailprocessor = new AcronisProcessor();
         return mailprocessor.ProcesMail(mail);
      case ......etc.
  }
}

person Luuk Krijnen    schedule 21.07.2015    source источник
comment
enum имеет фиксированную длину — вы не можете никоим образом расшириться — так что это точно так же, как оператор switch. Так что в некотором смысле он имеет те же ограничения. Не могли бы вы опубликовать свои enum и и примеры операторов if/switch, которые вы хотите заменить?   -  person Enigmativity    schedule 21.07.2015
comment
Я добавил часть кода уже в проекте.   -  person Luuk Krijnen    schedule 21.07.2015
comment
Значит, из Mail.MailType всегда возвращается MailResult?   -  person Enigmativity    schedule 21.07.2015
comment
Да, и результат почты — это класс, где MailType — это перечисление как свойство класса Mail.   -  person Luuk Krijnen    schedule 21.07.2015


Ответы (4)


То, что вы там реализовали, - это шаблон стратегии, который по-прежнему является допустимым подходом. В любом случае, если вы хотите заменить весь переключатель и сделать его более удобным в обслуживании, вы можете использовать этот

public interface IProcessMail
{
     bool CanProcess(MailType type);
     MailResult GetMailResult(Mail mail);
}

Каждый почтовый процессор реализует этот интерфейс. Тогда у вас будет это

 public class MailProcessorExecutor
 {
     public MailProcessorSelector(IEnumerable<IProcessMail> processors)
     {
           _processors=processors;
     }

     public MailResult GetResult(Mail mail)
     {
         var proc=_processor.FirstOrDefault(p=>p.CanProcess(mail.MailType));
         if (proc==null)
         {
             //throw something
         }

         return proc.GetMailResult(mail);
     }

    static IProcessMail[] _procCache=new IProcessMail[0];

     public static void AutoScanForProcessors(Assembly[] asms)
     {
       _procCache= asms.SelectMany(a=>a.GetTypesDerivedFrom<IProcessMail>()).Select(t=>Activator.CreateInstance(t)).Cast<IProcessMail>().ToArray();
     }

     public static MailProcessorExecutor CreateInstance()
     {
        return new MailProcessorExecutor(_procCache);
     }
  }

  //in startup/config 
  MailProcessorExecutor.AutoScanForProcessors([assembly containing the concrete types]);

 //usage
 var mailProc=MailProcessorExecutor.CreateInstance();
var result=mailProc.GetResult(mail);

Итак, дело в том, что будет объект, отвечающий за выбор и выполнение процессоров. Статические методы являются необязательными, метод AutoScan ищет во всех заданных сборках конкретные реализации IPocessMail, а затем создает их экземпляры. Это позволяет вам добавлять/удалять любой процессор, и он будет использоваться автоматически (никаких дополнительных настроек не требуется). Также здесь нет переключателя и никогда не будет. Обратите внимание, что GetTypesFromDerived — это вспомогательный метод, который я использую (это часть моей библиотеки CavemanTools) и активатора. требуется конструктор без параметров. Вместо этого вы можете использовать DI-контейнер для получения экземпляров (если вы используете один или у процессоров есть deps)

Если вы используете контейнер внедрения зависимостей, вам не нужен метод CreateInstance. Просто зарегистрируйте MailProcessorExecutor как синглтон и вставьте его (передайте его как аргумент конструктора) туда, где вам это нужно.

person MikeSW    schedule 21.07.2015
comment
Вы имеете в виду, что мой подход использовал Стратегию? Использует ли GetTypesDerivedFrom отражение (вы должны учитывать эффективность и безопасность типов)? - person ntohl; 21.07.2015
comment
Я имел в виду, что код OP похож на Strategy. Да GetTypesDerivedFrom использует отражение. Эффективность? :))) Вы даже не представляете, сколько вещей используют отражение под крышкой. Что касается безопасности типов, да, я забыл приведение (я обновил свой ответ, чтобы включить его). - person MikeSW; 21.07.2015
comment
Я пытался подумать о решении стратегии для вопроса ОП, но был бы переключатель, где контекст стратегии назначается фактически используемому AcronisBackupStrategy. Затем вызовите GetResult на нем. Я думаю, что стратегия не применима. Я не знаю, сколько рефлексии используется под прикрытием, но у меня был проект, в котором простое удаление 1 LINQ восстановило удобство использования программного обеспечения. - person ntohl; 21.07.2015
comment
Да, в коде ОП был переключатель, и его код — Стратегия. Мой комбинированный. О вашем 1 Linq, который был вашим конкретным крайним случаем. Кстати, только некоторые части отражения медленные, а то, что я использовал, - нет. - person MikeSW; 21.07.2015
comment
Еще не пробовал, но кажется законным решением моей проблемы. Я собираюсь попробовать! - person Luuk Krijnen; 23.07.2015
comment
Спасибо, сработало нормально! Я только заменил метод GetDerivedFrom на тот, на который ответил @DarrenKopp в следующем вопросе: stackoverflow.com/questions/26733/ - person Luuk Krijnen; 24.07.2015
comment
Downvoter, хотите прокомментировать? Возможно, у вас есть лучшее решение. - person MikeSW; 29.07.2015

В вашем случае простого рефакторинга в метод должно быть более чем достаточно.

private ISingleMailProcessor CreateMailProcessor(MailType type)
{
  switch (type)
  {
      case MailConnector.MailType.AcronisBackup:
         return new AcronisProcessor();
      case ......etc.
  }
}

public MailResult GetMailResult(mail)
{
  ISingleMailProcessor mailprocessor = CreateMailProcessor(mail.MailType);
  return mailprocessor.ProcesMail(mail);
}

Я не думаю, что использование factory поможет вам здесь. Фабрика имела бы смысл, если бы «тип почты» определялся вне кода, который фактически создает и отправляет саму почту.

Затем для каждого типа отправляемой почты будет определенная фабрика с интерфейсом для создания объекта отправителя. Но даже тогда это имело бы смысл, если бы вам каждый раз требовался новый экземпляр отправителя. В вашем случае просто передать интерфейс, который у вас есть прямо сейчас, должно быть более чем достаточно.

Вы можете прочитать мое мнение о фабриках здесь: https://softwareengineering.stackexchange.com/a/253264/56655

person Euphoric    schedule 21.07.2015
comment
Не поддавайтесь на оправдание YAGNI, упоминается в первом комментарии. Переключатель уже слишком большой, как заявил Луук. - person ntohl; 22.07.2015
comment
Я согласен, что основная проблема, которую я вижу, заключается в том, что создание экземпляра — это просто полностью независимая задача, которая не должна быть частью бизнес-домена. Независимо от того, КАК это извлечено, это должно быть извлечено - person Boas Enkler; 22.07.2015

Для этого вам понадобится шаблон Visitor.

{
    public MailResult GetMailResult(mail)
    {
        _objectStructure.Visit(mail)
    }
    ObjectStructure _objectStructure= new ObjectStructure();
    constructor() {
         _objectStructure.Attach(new AcronisBackupVisitor());
         _objectStructure.Attach(new ...)
    }
}

class AcronisBackupVisitor: Visitor {
    public override void Visit(HereComesAConcreteTypeDerivedFromMail concreteElement)
    {
         // do stuff
    }
    public override void Visit(HereComesAConcreteTypeDerivedFromMailOther concreteElement)
    {
        //don't do stuff. We are not in the right concrete mail type
    }
}

Таким образом, мы можем различать динамический тип конкретного Mail, который вы получаете. Просто сделайте Mail абстрактным и извлеките из него почту Acronis и другие типы почты. Я начал реализацию этого примера с здесь.

person ntohl    schedule 21.07.2015

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

Подводя итог веб-трансляции, вы могли видеть, что чем больше вы хотите следовать принципам OpenClosed в проблемной области «создания», тем менее безопасным для типов вы можете работать.

В результате абстрактная фабрика может также работать с некоторыми более динамическими значениями, такими как строки. Например, у вас может быть метод.

string GetAllPossibilities(); // Returns all possible kinds that can be constructed

и связанные

T Create<T>(string kind)

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

Таким образом, ваш вызов может быть примерно таким:

Factory.Create("Acronis") // Your Marker

Factory.Create("MYNameSpace.AcronisProcessor")  //TypeName

Factory.Create<AcronisProcessor>() //Type 

Таким образом, у вас не будет операторов switch вне Factory. Внутри фабрики у вас могут быть некоторые или вы могли бы подумать о создании какого-то динамического объекта.

В Factory все еще могут быть операторы swithc, переключающие ваш самодельный идентификатор или код, которые делают что-то вроде

var type = Type.GetType(kind);
return Activator.CreateInstance(type) as T;

Но поскольку это отделено от логики основного домена, оно больше не имеет такой важности, как раньше.

С таким на первый взгляд у вас не будет критических изменений API, если вы получите новые параметры.

Но есть еще какая-то глубинная семантическая зависимость.

РЕДАКТИРОВАТЬ: Как вы можете видеть в обсуждении ниже, я отрезал некоторые детали, так как я думаю, что они размывают основную мысль (OpenClosed Principales и Factory Pattern). Но есть еще некоторые моменты, о которых не следует забывать. Например, «Что такое корень приложения и как его устроить». Чтобы получить все подробности, веб-трансляция (а также другие на этом сайте) - гораздо лучший способ узнать эти подробности, чем этот пост здесь.

person Boas Enkler    schedule 21.07.2015
comment
В ответе даже не упоминается, где в примере находятся случаи переключения. В Вашем примере Create будет содержать регистр переключения в зависимости от kind. - person ntohl; 21.07.2015
comment
Я просто заметил несколько минут назад, и я прямо сейчас расширял свой пост, чтобы прояснить его. - person Boas Enkler; 21.07.2015
comment
ИМХО, это не очень хорошее использование factory. Вы связали статью дяди Боба, и У. Боб сказал, что переключатели должны содержаться в методе Main. И только для инициализации фабрик. Вы не передаете этот параметр для создания, а инициализируете фабрику с правильным типом. - person ntohl; 21.07.2015
comment
Хороший вопрос, но imo это действительно зависит от проблемной области. Конечно, я бы вообще старался избегать переключения, но я не понимаю, как это происходит в домене luuk. Также было бы возможно дать Фабрике IEnumerable‹Type› разрешенных типов или IENumerable‹Func‹Type, object››, но это очень подробно и imo не так важно, как сделать принцип более понятным. Например, если у него в настоящее время есть только два варианта, то для начала будет достаточно оператора if. (Помните об оптимизации и YAGNI). Главное, чтобы домен не соприкасался с корнем приложения - person Boas Enkler; 21.07.2015
comment
Это не я минусовал, просто для протокола. Мы не можем рассматривать ЯГНИ, потому что потребность уже есть. Переключатель слишком большой. - person ntohl; 21.07.2015
comment
нет проблем :) хм, я не вижу, насколько большой переключатель. Также его вопрос был сосредоточен на том, как реализовать шаблон factory, глядя на принцип openclosed :), но я согласен с вами по техническому содержанию :) - person Boas Enkler; 21.07.2015