如何使用 C# 在命令模式中的命令之间共享相同的上下文?

How to share the same context between commands in Command-Pattern with C#?

我已经在我的应用程序中实现了命令模式(以多支持方式)。

结构:

class MultiCommand : BaseCommand

abstract class BaseCommand : ICommand

处理流程:

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

   commandsGroup.Execute()

现在,假设在 Command1 中更改了 somethingID,我将在 Command2 中使用这个新值...而且,还有很多 在整个执行过程中受到影响的其他属性对象

此外,还有一些接口实现应该可以在任何命令中使用上下文对象,例如:

Context.ServerController.something();

IServerController 的实例化将在 multiCommandGroup 初始化之前进行。

如何为组中的所有命令共享一个 上下文

上下文示例 class:

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

    public CommandContext(){}
}

重要 最小的实现代码是 here

您可以在 BaseCommand class(及其派生的 classes)上有一个构造函数,它可以接受某种 Context class .在实例化属于同一组的命令时,您可以为它们提供所有相同的上下文对象。可能是这样的:

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

我建议做一些通用的东西。这是一个超级简单的例子。

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

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 中的命令取决于上一个命令的结果。在这种情况下,命令模式可能不是最好的。也许你应该尝试在这里实现中间件链?

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

那么改变你的方法呢?我最近为 DDD 做了一个架构,执行命令意味着原子操作(从持久性中检索聚合根,应用域规则并坚持聚合)所以我不需要共享上下文并且可以批量处理多个命令而无需担心。

Here 你有一个 cqrs 架构,它使用命令模式和我发布的上述策略。

我的 0.02:

1) MultiCommand class 看起来像 Composite pattern

您可能希望在基本命令 class 中添加 GetParentCommand() 方法,并在 MultiCommand class 中添加 AddChildCommand() 方法,它设置每个 children 的 parent。

然后 children 命令可以从其 parent 中获取上下文 object。 (Contextobject也应该定义在baseclass中。而且可以是泛型。)

编辑:

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) 我们可以创建一个全局单例上下文object。在 MultiCommand 的 Execute 函数中,我们可以在执行 children 的 Execute 函数之前设置当前上下文 object。然后 child 命令只能访问单例上下文 object。在执行完 children 之后,MultiCommand 可以重置上下文。 (这里的context其实就是一个stack。)

编辑:

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
     }
}

另一种选择是将上下文 object 作为 Execute 函数的参数:

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

考虑实用风格

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. 不需要激进的 OOP 设计模式
  7. 使用流畅的代码变得非常愉快
  8. 嵌套/装饰函数更容易想象和实现
  9. 你永远不会重复你自己
  10. Open/Closed 校长成为你的宗教
  11. 代码现在总是清晰、完整和简洁
  12. 有人甚至说不再需要接口

在您的情况下,如其他人所述,通过构造函数注入上下文就可以了。但总的来说,我会改为通过方法参数注入上下文:

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

    }
}

原因是:

  • 上下文应该由CommandGroup管理,这样我们就有更好的封装。
  • CommandGroup 负责执行其命令列表,因此 CommandGroup 可以向每个 Command 仅传递参数 Command确实需要,这些参数可能在运行时构造(可能是之前的Commands),这样就不可能传入这些对象作为我们构建命令列表的时间。因此,重用 Command 和简化单元测试变得更容易 Commands 因为我们不需要在单元测试中构造整个上下文对象。

也许你暂时不需要关心这些东西,但是方法注入提供了更多的灵活性。如果您使用过 .NET 中的某些框架,您会看到类似于 OwinContextFilterContext、.. 它们作为参数传递并包含该上下文的相关信息。

在我看来,您的案例不适合命令模式。命令表示用户请求(操作),这些对象可以在运行时动态创建,但您是在编码时预定义命令。

您尝试做的事情看起来像 owin 中间件或 asp.net 网络 api 消息处理程序,它们是 http://www.dofactory.com/net/chain-of-responsibility-design-pattern