Обработка пакетов команд - PullRequest
1 голос
/ 12 апреля 2019

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

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  &&
                    handlerInterface.IsAssignableFrom(t)
                ).First();

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

Опция 3 То же, что и опция 2, но также аннотировать все обработчики с помощью пользовательской аннотации, а также при поиске фильтра типов по аннотации.

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

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

1 Ответ

0 голосов
/ 12 апреля 2019

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

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

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

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

...