Как разделить один и тот же контекст между командами в Command-Pattern с C#?

я реализовал шаблон Command (в Multi-поддержка сторону) в моем приложении.

структура:

class MultiCommand : BaseCommand

abstract class BaseCommand : ICommand

Процесс:

   var commandsGroup = new MultiCommand(new List<ICommand>()
            {
                new Command1(),
                new Command2(),
                new Command3(),
            });

   commandsGroup.Execute()

теперь предположим, что в Command1 a somethingID меняется и я буду использовать это новое значение в Command2... А также, что есть много другие свойства и объекты которые затрагиваются во время всего процесса выполнения.

кроме того, есть какой-то интерфейс реализации, которые должны быть доступны в любой команде, просто используя объект контекста, например:

Context.ServerController.something();

реализация IServerController состоится непосредственно перед multiCommandGroup инициализации.

как я могу иметь общий контекст как это для всех команд группы?

пример класса Context:

public class CommandContext
{
    public IServerController ServerController;
    public RequiredData Data { get; set; }

    public CommandContext(){}
}

важно Минимальный код реализации здесь

7 ответов


1) Если вы хотите сохранить этот интерфейс, то вы должны передать этот контекст в качестве параметра конструктора:

new MultiCommand(new List<ICommand>()
            {
                new Command1(context),
                new Command2(context),
                new Command3(context),
            })

2) в качестве другого варианта вы можете принять список делегатов вместо списка команд. MultiCommand будет выглядеть так:

class MultiCommand : ICommand
{
    public MultiCommand(List<Func<Context, Command>> commands, Context context)

}

это почти то же, кроме MultiCommand отвечает за все команды используют тот же контекст.

3) выглядит как команды в MultiCommand зависит от результата предыдущей команды. В этом деле Командный шаблон, вероятно, не лучший. Может быть, вам стоит попробовать реализовать Middleware chain здесь?

interface IMiddleware<TContext>
{
   void Run(TContext context);
}

class Chain<TContext>
{
    private List<IMiddleware<TContext>> handlers;

    void Register(IMiddleware<TContext> m);

    public void Run(TContext context)
    {
        handlers.ForEach(h => h.Run(context));
    }
}

Я бы предложил сделать что-то родовое. Вот очень простой пример.

class MultiCommand<TContext>  
{
    List<Command<TContext>> Commands;
    TContext Context;
}

у вас может быть конструктор на вашем BaseCommand класс (и его производные классы), который принял бы Context класс какой-то. При создании экземпляров команд, которые будут принадлежать к одной группе, вы можете предоставить им один и тот же объект контекста. Может быть, что-то вроде:

public class CommandContext
{
    // The object that will be the target of the commands' actions.
    public object Data { get; set; }

    // ... any other properties that might be useful as shared state between commands...
}

public abstract class BaseCommand : ICommand
{
    protected CommandContext Context { get; private set; }

    public BaseCommand(CommandContext ctx)
    {
        Context = ctx;
    }
}

public class ChangeSomethingIDCommand : BaseCommand
{
    public ChangeSomethingIDCommand(CommandContext ctx) : base(ctx)
    { }

    public void Execute()
    {
        var target = (SomeDomainClass)Context.Data;
        target.SomethingID++;
    }
}

// Elsewhere in your code (assuming 'myTargetDomainClassInstance' is
// a SomeDomainClass instance that has been instantiated elsewhere and
// represents the object upon which the commands will do work):
var ctx = new CommandContext { Data = myTargetDomainClassInstance };
var commandGroup = new MultiItemCommand(ctx, new List<ICommand>
    {
        new ChangeSomethingIDCommand(ctx),
        new Command2(ctx),
        new Command3(ctx)
    });

commandGroup.Execute();

рассмотрим функциональный стиль

public class SomeMainClass{
   public void MultiCommandInit()
    {
        MultiCommand.New()
            .Add(new Command1())
            .Add(new Command2())
            .Add(new Command3())
            .SharedContext(CC => {

                CC.Data = new RequiredData();
                CC.ServerController = GetServerController();
            });

    }

    private IServerController GetServerController()
    {
        // return proper instance of server controller
        throw new NotImplementedException();
    }
}

требуется этот метод / функция расширения...

  public static class XMultiCommand
    {
        //  How can I have a shared context like this for all Commands of the group?
        public static MultiCommand SharedContext(this MultiCommand mc, Action<CommandContext> CallBack)
        {
            var cc = new CommandContext();            
            CallBack(cc);
            mc.SharedContext = cc;
            return mc;
        }

    }

наконец, эти изменения в MultiCommand

public class MultiCommand
{
    private System.Collections.Generic.List<ICommand> list;
    public List<ICommand> Commands { get { return list; } }
    public CommandContext SharedContext { get; set; }

    public MultiCommand() { }
    public MultiCommand(System.Collections.Generic.List<ICommand> list)
    {
        this.list = list;
    }
    public MultiCommand Add(ICommand cc)
    {
        list.Add(cc);
        return this;
    }

    internal void Execute()
    {
        throw new NotImplementedException();
    }
    public static MultiCommand New()
    {
        return new MultiCommand();
    }
}

Классные Вещи Происходят С Использованием Функциональных Стилей

  1. повторное использование парит!
  2. гипер фокус на одной ответственности касается
  3. композиция становится нормой
  4. обслуживание кода становится просто
  5. Intellisense становится вашим встроенным API (просто используйте комментарий кода)
  6. никакие радикальные картины дизайна ООП не необходимы
  7. беглый код становится очень приятным для работы с
  8. вложенные / украшенные функции гораздо проще представить и реализовать
  9. вы никогда не повторите youerself
  10. открытый / закрытый Принципал становится вашей религией
  11. код теперь всегда ясен, завершен и Краткий
  12. некоторые даже говорят, что интерфейсы больше не нужны

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

public class Command1: BaseCommand
{
    //inject as parameter instead
    public void Execute(Context ctx)
    {

    }
}

причины:

  • контекст должен управляться CommandGroup чтобы у нас была лучшая инкапсуляция.
  • на CommandGroup отвечает за выполнение своего списка команд, так что это возможно для CommandGroup перейти к Каждый Command только параметры каждого Command очень нужно, эти параметры могут быть построены в время работы (возможно, по ) так что невозможно передать эти объекты как время, когда мы создаем список команд. Таким образом, проще повторно использовать Command а также упростить модульное тестирование этих Commands поскольку нам не нужно создавать весь объект контекста в модульных тестах.

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

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

то, что вы пытаетесь сделать, похоже на промежуточное ПО owin или asp.net обработчик сообщений Web api, которые являютсяhttp://www.dofactory.com/net/chain-of-responsibility-design-pattern


а как насчет изменения вашего подхода? Недавно я сделал архитектуру для DDD и выполнил запятую, подразумевающую атомарную операцию (извлечение aggregate root из persitence, применение правил домена и pesist aggregate), поэтому я не нуждаюсь в общем контексте и могу паковать несколько команд без забот.

здесь у вас есть архитектура cqrs, которая использует шаблон команды с вышеуказанной стратегией, которую я опубликовал.


моя 0.02:

1) класс MultiCommand выглядит как составной шаблон.

возможно, вы захотите добавить метод GetParentCommand() в базовый класс команд и добавить метод AddChildCommand () в класс MultiCommand, который устанавливает родительский элемент каждого ребенка.

тогда дочерние команды могут получить объект контекста от его родителя. (Объект Context также должен быть определен в базовом классе. И он может быть общего типа.)

edit:

abstract class BaseCommand<T> : ICommand
{
    public T Context { get; set; }
    public BaseCommand Parent { get; set; }
}

class MultiCommand : BaseCommand 
{
     public void AddChildCommand(BaseCommand command) 
     {
         command.parent = this; // we can get parent's context from children now
         // put the command in an internal list
     }
}

var commandsGroup = new MultiCommand();
commandsGroup.AddChildCommand(new Command1());
commandsGroup.AddChildCommand(new Command2());
commandsGroup.AddChildCommand(new Command3());

commandsGroup.Execute()

2) мы можем создать глобальный синглтон объект контекста. В функции Execute MultiCommand мы могли бы установить текущий объект контекста перед выполнением дочерней функции Execute. Затем дочерняя команда может просто получить доступ к одноэлементному объекту контекста. И после выполнения всех детей, MultiCommand может сбросить контекст. (Контекст на самом деле является стеком.)

edit:

abstract class BaseCommand : ICommand
{
     // it could be put anywhere else as long as it can be accessed in command's Execute
     // it can also be a stack
     public static CommandContext Context {get; set;} 
}

class MutliCommand : BaseCommand 
{
    public void Execute()
    {
        // do something to BaseCommand.Context

        ChildCommand.Execute();

        // do something to BaseCommand.Context
    }
}

class ChildComand: BaseCommand 
{
     void Execute() 
     {
          // do something with BaseCommand.Context
     }
}

другой вариант-поместить контекст объект как параметр функции Execute:

class MultiCommand : BaseCommand 
{
     void Execute(CommandContext context) 
     {
         Children.Execute(context);
     }
}