Обработка пакетов команд

#c# #oop #design-patterns #architecture

#c# #ооп #шаблоны проектирования #архитектура

Вопрос:

У меня есть архитектура, подобная той, которая описана в этом сообщении в блоге . Таким образом, у меня есть объекты command, например :

 public class MoveCustomerCommand : ICommand
{
    public int CustomerId { get; set; }
    public Address NewAddress { get; set; }
}
  

И обработчики команд для каждой команды, которые выводятся из интерфейса ICommandHandler<TCommand> :

 public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand>
{
    public void Handle(MoveCustomerCommand command)
    {
        // Logic here
    }
}
  

Теперь я ищу чистое решение для следующего варианта использования: некоторые клиенты создают пакет разнородных команд, которые необходимо обработать. Другими словами, я хочу реализовать следующее:

 void HandleBatch(List<ICommand> batch) {
}
  

У меня есть несколько идей, но я не уверен, что какая-либо из них достаточно хороша.

Вариант 1 Поместите в HandleBatch функцию огромный переключатель.

 void HandleBatch(List<ICommand> batch) {
  foreach (var command in batch) {
    switch (command) {
      case MoveCustomerCommand cmd:
        new MoveCustomerCommandHandler().Handle(cmd);
        break;
      case DeleteCustomerCommand cmd:
        new DeleteCustomerCommandHandler().Handle(cmd);
        break;
      // ....
    }

  }
}
  

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

 void HandleBatch(List<ICommand> batch) {
  foreach (var command in batch) {
    var commandType = command.GetType();

    var handlerInterface = typeof(ICommandHandler<>)
      .MakeGenericType(new Type[]{commandType});

    // Search the current assembly for a type that implements "handlerInterface" 
    var handlerType = Assembly.GetAssembly(this.GetType())
                .GetTypes()
                .Where(t => t != handlerInterface  amp;amp;
                    handlerInterface.IsAssignableFrom(t)
                ).First();

    var handler = CreateInstance(handlerType);
    handler.Handle(command);
  }
}

  

Вариант 3 Такой же, как вариант 2, но также аннотирует все обработчики пользовательской аннотацией и при поиске типа фильтрует по аннотации.

Вариант 4 Что-то еще?

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

Комментарии:

1. Мой плохой, все команды реализуют ICommand и ICommandHandler<TCommand> таковы, что TCommand реализуют ICommand . Я обновил вопрос.

2. Обновление: добавлен пример кода в вопросе.

3. В другом сообщении в блоге Стивена есть то, что вы ищете (см. CommandService )

4. Я имел дело с этой ситуацией в прошлом, используя то, что я бы назвал CommandTable . Это достаточно масштабируемо по сравнению с механизмом отражения, но не имеет таких же накладных расходов, как отражение. Излишне говорить, switch case что механизм не самый удобный в обслуживании

5. @Vikhram можете ли вы предоставить более подробную информацию об этом CommandTable ? Это просто словарь ICommand to ICommandHandler или что-то более умное? Кроме того, каковы накладные расходы на отражения?

Ответ №1:

ОК. Предположим, у вас есть следующие команды:

 public class MoveCustomerCommand : ICommand
    {
        public int CustomerId { get; set; }

        public bool CanExecute(object parameter) => true;

        public void Execute(object parameter) { }


        public event EventHandler CanExecuteChanged;
    }

public class KillCustomerCommand : ICommand
    {
        public int CustomerId { get; set; }

        public bool CanExecute(object parameter) => true;

        public void Execute(object parameter) { }


        public event EventHandler CanExecuteChanged;
    }
  

Теперь рассмотрим следующее предложение по архитектуре для обработчиков:

  public abstract class CommandHandlerBase
    {
        protected static readonly Dictionary<Type, CommandHandlerBase> _handlers = new Dictionary<Type, CommandHandlerBase>();

        protected abstract void HandleCommand<TCommand>(TCommand command) where TCommand: ICommand;

        public static void Handle<TCommand>(TCommand command) where TCommand : ICommand
        {
            if (_handlers.TryGetValue(typeof(TCommand), out var handler))
            {
                handler.HandleCommand(command);
            }
        }
    }

    public abstract class CommandHandlerBase<TCommandHandlerBase, TCommand> : CommandHandlerBase
        where TCommandHandlerBase : CommandHandlerBase<TCommandHandlerBase, TCommand>, new() where TCommand : ICommand
    {
        public static void Register()
        {
            var type = typeof(TCommand);
            _handlers[type] = new TCommandHandlerBase();
        }

        protected override void HandleCommand<T>(T command) => Handle((TCommand) (object) command);

        public abstract void Handle(TCommand command);
    }
  

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

Реализация может показаться запутанной на первый взгляд, но использование действительно приятно после: мы только определяем наши классы обработчиков и вызываем Register . Давайте посмотрим, как они выглядят:

 public class MoveCustomerCommandHandler : CommandHandlerBase<MoveCustomerCommandHandler, MoveCustomerCommand>
    {
        public override void Handle(MoveCustomerCommand command) => Console.WriteLine("Moving the customer");
    }

    public class KillCustomerCommandHandler : CommandHandlerBase<KillCustomerCommandHandler, KillCustomerCommand>
    {
        public override void Handle(KillCustomerCommand command) => Console.WriteLine("Killing the customer");
    }
  

И тестирование:

 private static void Main(string[] args)
{
    MoveCustomerCommandHandler.Register();
    KillCustomerCommandHandler.Register();
    CommandHandlerBase.Handle(new MoveCustomerCommand());
    CommandHandlerBase.Handle(new KillCustomerCommand());
    Console.ReadLine();
}
  

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

Кроме того, вы можете позже добавить метод отмены регистрации или сохранить более одного обработчика для данной команды, предел — это небо .. =)