использование командных и фабричных шаблонов проектирования для выполнения заданий в очереди

У меня есть список заданий, поставленных в очередь в базе данных, которые мне нужно прочитать из базы данных и выполнить их параллельно с использованием потоков, и у меня есть список классов команд для выполнения каждого из этих заданий, реализующих общий интерфейс (шаблон команды). но когда я извлекаю ожидающие задания из базы данных, мне нужно будет создать экземпляр правильного объекта команды для каждого задания примерно так (в фабричном классе)

ICommand command;
switch (jobCode)
{
  case "A":
     command = new CommandA();
     break;
  case "B":
     command = new CommandB();
     break;
  case "C":
     command = new CommandC();
     break;
}

command.Execute();

Есть ли лучший способ создать правильный объект команды без использования большого оператора switch, как указано выше? ИЛИ есть ли другой шаблон для выполнения заданий в очереди?

Решение: решено так (на основе выбранного ответа). Это сделает ленивое создание экземпляров командных объектов.

public class CommandFactory
{
    private readonly IDictionary<string, Func<ICommand>> _commands;

    public CommandFactory()
    {
        _commands = new Dictionary<string, Func<ICommand>>
                        {
                            {"A", () => new CommandA()},
                            {"B", () => new CommandB()},
                            {"C", () => new CommandC()}
                        };
    }

    public ICommand GetCommand(string jobKey)
    {
        Func<ICommand> command;
        _commands.TryGetValue(jobKey.ToUpper(), out command);
        return command();
    }
}    

Client: 

        var factory = new CommandFactory();
        var command = factory.GetCommand(jobKey);
        command.Execute();

person RKP    schedule 09.01.2012    source источник
comment
Это кажется ошибочным, учитывая, что все ваши команды должны быть на вашей фабрике.   -  person KingOfHypocrites    schedule 06.11.2013


Ответы (3)


Большинство реализаций шаблонов команд C# более или менее совпадают с реализацией Java. Эти реализации обычно используют интерфейс ICommand:

public interface ICommand
{
    void Execute();
}

и тогда все классы команд вынуждены реализовывать интерфейс. У меня нет проблем с этим решением, но лично мне не нравится создавать слишком много классов, и вместо этого я предпочитаю использовать делегатов .NET (в Java нет делегатов). Делегат Action обычно помогает, если нужна только одна ссылка на метод:

public class Prog
{
    public Prog()
    {
        var factory = new CommandFactory();
        factory.Register("A", () => new A().DoA);            
        factory.Register("B", () => new B().DoB);
        factory.Register("C", DoStuff);

        factory.Execute("A");
    }

  public static void DoStuff()
    {
    }
}

public class CommandFactory
{
    private readonly IDictionary<string, Action> _commands;       

    public void Register(string commandName, Action action)
    {
    _commands.Add(commandName, action); 
    }

    public Action GetCommand(string commandName)
    {
        _commands[commandName];
    }

    public void Execute(string commandName)
    {
        GetCommand(commandName)();
    }
}
public class A
{
    public void DoA()
    {
    }
}

public class B
{
    public void DoB()
    {
    }
}

Если вашему командному интерфейсу требуется более одного метода, например:

public interface ICommand
{
    void Execute();
    void Undo();
}

Вы можете использовать класс-оболочку следующим образом:

public class Command
{
    public Command(Action execute, Action undo)
    {
        Execute = execute;
        Undo = undo;
    }

    public Action Execute { get; protected set; }
    public Action Undo { get; protected set; }
}

или (неважно какой)

public class Command 
{
    private readonly Action _execute;
    private readonly Action _undo;

    public Command(Action execute, Action undo)
    {
        _execute = execute;
        _undo = undo;
    }

    public void Execute()
    {
        _execute();
    }

    public void Undo()
    { 
        _undo();
    }
}

(это может даже реализовать ICommand, если у вас уже есть устаревшие вещи, использующие его. Если вы используете интерфейс, фабрика должна использовать интерфейс вместо класса Command)

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

public class Prog2
{
    public Prog2()
    {
        var factory = new CommandFactory2();
        factory.Register("A", new Lazy<Command>(
            ()=>
                {
                    var a = new A();
                    return new Command(a.DoA, a.UndoA);
                }));

        factory.Register("B", new Lazy<Command>(
           () =>
           {
               var c = new B();
               return new Command(c.DoB, c.DoB);
           }));

        factory.Register("C", new Lazy<Command>(
            () => new Command(DoStuff, UndoStuff)));

        factory.Execute("A");
    }

    public static void DoStuff()
    {
    }

    public static void UndoStuff()
    {
    }
}

public class CommandFactory2
{
    private readonly IDictionary<string, Lazy<Command>> _commands;

    public void Register(string commandName, Lazy<Command> lazyCommand)
    {
        _commands.Add(commandName, lazyCommand);
    }

    public void Register(string commandName, Action execute, Action undo)
    {
        _commands.Add(commandName, new Lazy<Command>(() => new Command(execute, undo)));
    }

    public Command GetCommand(string commandName)
    {
        return _commands[commandName].Value;
    }

    public void Execute(string commandName)
    {
        GetCommand(commandName).Execute();
    }

    public void Undo(string commandName)
    {
        GetCommand(commandName).Undo();
    }
}


public class A
{
    public void DoA()
    {
    }

    public void UndoA()
    {
    }
}

public class B
{
    public void DoB()
    {
    }

    public void UndoB()
    {
    }
}

Как видите, нет необходимости реализовывать интерфейс, даже если у вас есть более одного метода (Execute, Undo и т. д.). Обратите внимание, что методы Execute и Undo могут принадлежать к разным классам. Вы можете структурировать свой код так, как он кажется более естественным, и по-прежнему можете использовать шаблон команды.

person Jeno Laszlo    schedule 09.01.2012
comment
предоставленному вами примеру не хватает гибкости дизайна и возможности повторного использования, предоставленный вами пример подталкивает пользователя к созданию более похожего на жестко закодированный класс. Я предпочитаю пример @Slade, он лучше. - person Usman Masood; 09.01.2012
comment
Пожалуйста, объясните, почему ему не хватает гибкости дизайна и возможности повторного использования? Вы можете добавить в словарь любой метод, который удовлетворяет сигнатуре делегата. Что вы подразумеваете под более похожим на жестко закодированный класс? - person Jeno Laszlo; 09.01.2012
comment
factory.Register(C Это один из тех жестких кодов, который является большим недостатком в дизайне. Так что не делайте жесткого кода. - person Zenwalker; 09.01.2012
comment
Я не понимаю, даже в примере со Слэйдом нужно указывать ключи типа factory.RegisterCommand‹SomeCommand›(C). Позвольте мне отметить, что мой образец позволяет не заставлять вас создавать экземпляр, плюс вы также можете регистрировать методы непустых классов конструктора. - person Jeno Laszlo; 09.01.2012
comment
Спасибо. как это можно улучшить для ленивого создания экземпляров - person RKP; 09.01.2012
comment
Я также добавил ленивую реализацию. Вам нужен словарь строк и Lazy‹Action›. Я не думаю, что мое решение менее гибкое: вы не ограничены классами команд без параметров и вам не нужно реализовывать интерфейс. Вы даже можете зарегистрировать статические методы, если хотите. - person Jeno Laszlo; 09.01.2012
comment
это помогло. Спасибо. можно ли это изменить только для получения экземпляра объекта команды вместо получения дескриптора его метода выполнения? потому что я думаю, что factory должен заниматься только созданием объектов, а не их выполнением (SRP). - person RKP; 09.01.2012
comment
Лучше всего то, что, поскольку вы используете функциональный стиль, вы даже можете использовать замыкания; вы можете создать экземпляр целевого класса только один раз: codethinked.com/c-closures-explained Я мог бы написать пример позже, который демонстрирует это. - person Jeno Laszlo; 09.01.2012
comment
Я добавил GetCommand, который вернет вам делегата Action по имени. - person Jeno Laszlo; 09.01.2012
comment
Я имею в виду, может ли GetCommand вернуть командный объект, реализующий ICommand? в вашем примере он возвращает делегат методу Execute. - person RKP; 09.01.2012

Вы можете использовать Dictionary для сопоставления буквы/символа с соответствующей реализацией ICommand. Что-то типа:

public class CommandFactory
{
    private readonly Dictionary<string, ICommand> mCommands = new Dictionary<string,ICommand>(StringComparer.OrdinalIgnoreCase);

    public void RegisterCommand<TCommand>(string commandKey) where TCommand : ICommand, new()
    {
        // Instantiate the command
        ICommand command = new TCommand();

        // Add to the collection
        mCommands.Add(commandKey, command);
    }

    public void ExecuteCommand(string commandKey)
    {
        // See if the command exists
        ICommand command;
        if (!mCommands.TryGetValue(commandKey, out command))
        {
            // TODO: Handle invalid command key
        }

        // Execute the command
        command.Execute();
    }
}

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

ИЗМЕНИТЬ

В ответ на ваш комментарий, чтобы создавать экземпляры только при выполнении, вы можете сделать что-то вроде:

public class CommandDetails<T> where T : ICommand, new()
{
    private ICommand mCommand;

    public ICommand GetCommand()
    {
        if (/* Determine if the command has been instantiated */)
        {
            // Instantiate the command
            mCommand = new T();
        }

        return mCommand;
    }
}

public void ExecuteCommand(...)
{
    // See if the command exists
    CommandDetails details;
    // ...

    // Get the command
    // Note: If we haven't got the command yet, this will instantiate it for us.
    ICommand command = details.GetCommand();

    // ...
}
person Samuel Slade    schedule 09.01.2012
comment
Спасибо. это классический пример шаблона команды, в котором все объекты команды хранятся в коллекции в классе-контейнере, но как его можно улучшить, чтобы создавать их экземпляры только тогда, когда они используются? - person RKP; 09.01.2012
comment
но как я могу зарегистрировать команду (добавить команду в словарь) в первую очередь без ее создания? один из вариантов - использовать делегатов, как указано в другом ответе. интересно узнать, есть ли еще решения. - person RKP; 09.01.2012
comment
В моем расширенном ответе регистрация будет работать почти так же, за исключением того, что вы будете хранить CommandDetails в файле Dictionary. Чтобы обойти отсутствие параметров общего типа, вы можете реализовать в CommandDetails общий интерфейс ICommandDetails, который будет иметь только метод с сигнатурой: ICommand GetCommand();. - person Samuel Slade; 09.01.2012

Вы можете попросить свою работу предоставить собственную команду ICommand:

interface IJob 
{
  ICommand Command { get; }
}

public class JobA : IJob
{
  private readonly ICommand _command = new CommandA();
  public ICommand Command { get { return _command; } }
}

Тогда вместо включения jobCode вы можете просто сделать:

job.Command.Execute();
person Rich O'Kelly    schedule 09.01.2012
comment
не уверен, что это поможет. вместо того, чтобы переключаться для создания объекта команды, мне нужен переключатель для создания правильного объекта задания, используя это. - person RKP; 09.01.2012
comment
@RKP Я предполагал, что ваш jobCode будет исходить из задания - в момент создания экземпляра задания вы знаете, какой тип задания вы хотите и, следовательно, какую команду также. - person Rich O'Kelly; 09.01.2012
comment
Дело снова доходит до обсуждения что создать вместо ICommand теперь ему нужно заглянуть в IJob :) - person Usman Masood; 09.01.2012
comment
Вы нашли лучший шаблон дизайна для этой проблемы? Спасибо - person cyberjoac; 19.02.2017