使用C#的Action委托实现命令模式

7

是否可以使用Action委托队列来实现GOF命令模式?

我已经尝试了一段时间,但是因为我想要添加到队列中的每个可能的操作都可能具有不同数量的参数,所以我感到困惑。

有什么建议吗?我是否错在专注于命令模式?

更新:

非常感谢jgauffin,它完美地工作了...我的实现现在看起来像:

public class CommandDispatcher
{
    private readonly Dictionary<Type, List<Action<ICommand>>> _registeredCommands =
        new Dictionary<Type, List<Action<ICommand>>>();

    public void RegisterCommand<T>(Action<ICommand> action) where T : ICommand
    {
        if (_registeredCommands.ContainsKey(typeof (T)))
            _registeredCommands[typeof (T)].Add(action);
        else
            _registeredCommands.Add(typeof (T), new List<Action<ICommand>> {action});
    }

    public void Trigger<T>(T command) where T : ICommand
    {
        if (!_registeredCommands.ContainsKey(typeof(T)))
            throw new InvalidOperationException("There are no subscribers for that command");

        foreach (var registeredCommand in _registeredCommands[typeof(T)])
        {
            registeredCommand(command);
            if (command.Cancel) break;
        }
    }
}
4个回答

10

您可以使用一个Action,但不应使用多个参数。如果一个命令需要一个新的参数,那么您需要更改调用该命令的所有位置以及处理程序。

相反,您应该使用具有所有参数作为属性的Command类。这样,您可以添加参数而不影响代码(在处理程序中,新参数应被视为可选)。

以下是我会这样做的:

public interface ICommand
{
    // Cancel processing, do not invoke any more handlers
    public bool Cancel { get; set; }
}

public class CommandDispatcher 
{
  private Dictionary<Type, List<Action<ICommand>>> _commands = new Dictionary<Type, List<Action<ICommand>>>();


  // Add to dictionary here
  public void Subscribe<T>(Action<T> action) where T : ICommand
  {
      List<Action<ICommand>> subscribers;
      if (!_commands.TryGetValue(typeof(T), out subscribers))
      {
          subscribers = new List<Action<ICommand>>();
          _commands.Add(typeof(T), subscribers));
      }

      subscribers.Add(action);
  }

  // find command and to foreach to execute the actions      
  public void Trigger<T>(T command) where T : ICommand
  {
      List<Action<ICommand>> subscribers;
      if (!_commands.TryGetValue(typeof(T), out subscribers))
          throw new InvalidOperationException("There are no subscribers for that command");

      foreach(var subsriber in subscribers)
      {
          subscriber(command);
          if (command.Cancel)
              break; //a handler canceled the command to prevent others from processing it.
      }
  }

}

public class AddTextCommand : ICommand
{
    public string TextToAdd {get;set;}
}

public class TextHandler
{
    public TextHandler(CommandDispatcher dispatcher)
    {
        disptacher.Subscribe<AddTextCommand>(OnAddText);
    }

    public void OnAddText(AddTextCommand cmd)
    {
        //....
    }
}


public partial class MyForm : Form
{
    CommandDispatcher _dispatcher;

    private void MyTextBox_Changed(object source, EventArgs e)
    {
        _dispatcher.Trigger(new AddTextCommand{TextToAdd = MyTextBox.Text}=;
    } 
}

请注意,此代码为伪代码。我直接在答案中编写了它,但没有测试过它。你可能需要更改一些东西才能使其正常工作,但至少可以给你一些提示。该实现允许你为每个命令添加多个订阅者。


这似乎是一个有趣的解决方案,您介意详细说明触发方法如何工作吗?是command(T)还是command.Invoke()?为什么要使用_commands字典,而不是直接使用List或Queue? - David
字典更快,您不必自行遍历集合以查找正确的命令类型(及其订阅者)。 - jgauffin
我已经添加了实现,它们可能不会百分之百地工作。请尽力让它运行起来。 - jgauffin
如果您不想自己实现它,可以使用我的 Griffin.Decoupled:http://blog.gauffin.org/2012/10/introducing-griffin-decoupled/ - jgauffin

3
在命令模式中,典型的命令接口将具有简单的执行方法 - 这可以用Action委托表示。但是,实际的实现将由不同的具体类提供,您可以通过构造函数传递参数(例如)。例如:
public interface ICommand 
{
   public void Execute();
}

public class Command1 : ICommand
{
   public Command1(int param1, string param2)
   {
   }

   ...
}

public class Command2 : ICommand
{
  ...
}

public class Program
{

   public static void Main()
   {

       ...

       var commands = new List<Action>();
       commands.Add((new Command1(3, "Hello")).Execute);
       commands.Add((new Command2(...)).Execute);

       ...
   }


}

这里的重点是,与命令相关的状态和实现将被封装在不同的实现中,而Action委托将指向其实例方法。因此,调用该委托将导致执行命令。

1
如果你关心参数数量的话,那么使用一个类来正确地实现命令模式是正确的选择。而 Action 委托只能限定于一个。此外,如果你使用 Action 委托,你可能会想要实现撤销操作,但由于你只使用了一个委托而不是一个类,你将无法实现撤销操作。

0

另一种“极简主义”的方法是利用C#闭包的属性自动捕获所需的参数。 之所以称之为极简主义,是因为这里甚至没有任何ICommand接口(因为我们直接使用Action),更不用说实现了。在某些情况下可能很有用。

请参见以下LinqPad示例:

    void Main()
    {
        var commands = new List<Action>();
    
        commands.Add(MakeCommandOne("CommandOne"));
        commands.Add(MakeCommandTwo("CommandTwo", 5));
    
        var p3_1 = "parameter3_1";
        var p3_2 = 32;
        var name3 = "CommandThree";
        commands.Add(
        () => Console.WriteLine($"Name: {name3}, p1: {p3_1}, p2: {p3_2}")
        );
        
        RunAllCommands(commands);
    }
    
    void RunAllCommands(List<Action> commands)
    {
        foreach (var command in commands)
        {
            command();
        }
    }
    
    Action MakeCommandOne(string name) => () => Console.WriteLine($"Name: {name}");
    Action MakeCommandTwo(string name, double age) => () => Console.WriteLine($"Name: {name}, age: {age}");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接