quarta-feira, 11 de Novembro de 2009

Ramblings on the Command design pattern

I was recently in need of implementing a mechanism for Undo/Redo for a GUI and was led to reminisce on the good old Command design pattern. Basically, I led myself to imagine what was the cleanest and most minimalist way of implementing the pattern in the .NET framework, specifically taking advantage of the C# 3.0 approach to functional programming.

It turns out implementing a generic Command pattern in C# 3.0 is so easy and powerful, via a clever use of delegates, that even the need for some of the classes specified in the original pattern is virtually eliminated.

Here's the implementation of a CommandExecutor class, which single-handedly deals with all the reusable aspects of the pattern:

public class CommandExecutor
{
  private int currentCommand = -1;
  private readonly List<Command> history = new List<Command>();

  public event EventHandler StatusChanged;

  public bool CanUndo
  {
    get { return currentCommand >= 0; }
  }

  public bool CanRedo
  {
    get { return currentCommand < history.Count - 1; }
  }

  public void Execute(Action command, Action undo)
  {
    if (command == null)
    {
      throw new ArgumentNullException("command");
    }

    command();
    if (undo != null)
    {
      history.RemoveRange(
        ++currentCommand,
        history.Count - currentCommand
      );
      history.Add(new Command(command, undo));
    }
    else
    {
      history.Clear();
      currentCommand = -1;
    }

    OnStatusChanged(EventArgs.Empty);
  }

  public void Undo()
  {
    if (CanUndo)
    {
      history[currentCommand--].Undo();
      OnStatusChanged(EventArgs.Empty);
    }
  }

  public void Redo()
  {
    if (CanRedo)
    {
      history[++currentCommand].Execute();
      OnStatusChanged(EventArgs.Empty);
    }
  }

  protected virtual void OnStatusChanged(EventArgs e)
  {
    var handler = StatusChanged;
    if (handler != null)
    {
      handler(this, e);
    }
  }

  private class Command
  {
    private readonly Action execute;
    private readonly Action undo;

    public Command(Action execute, Action undo)
    {
      this.execute = execute;
      this.undo = undo;
    }

    public Action Execute
    {
      get { return this.execute; }
    }

    public Action Undo
    {
      get { return this.undo; }
    }
  }
}

With this functor based implementation, commands can be easily passed into the executor along with the undo method, either as references to existing methods, or as lambda expressions.

Using the delegate indirection, this type of implementation conveniently eliminates the need for implementing a common Command interface, which makes it that much more reusable, as you can easily pass in methods of classes which you did not implement, or very easily and concisely transform specific method calls into commands without the need to implement yet another class.

Also, using the scoping semantics of anonymous delegates, you can easily use closures as data members for sharing immutable data between the execution of the command and the undo operation. For instance, consider the following example:

public class Test
{
  private readonly CommandExecutor executor = new CommandExecutor();

  public void TestScoping()
  {
    int sum = 0;

    for (int i = 0; i < 10; ++i)
    {
      var scopedData = i;
      executor.Execute(
        () => sum += scopedData,
        () => sum -= scopedData
      );
    }

    for (int i = 0; i < 10; ++i)
    {
      executor.Undo();
    }
  }
}

In this example, scopedData is used in the closure of the command delegates. Each iteration of the for-loop produces a different numeric value, which is implicitly preserved onto each command execution. By undoing the command, the sum gets preserved.

If instead the for-loop counter variable was passed, the undo commands would not be adequately preserved, as that variable would have been shared between all closures of all commands. In this way, you can adequately choose the level of sharing between command executions and undo operations, and effectively use the principles of functional programming to insulate each command execution from side-effects, while at the same time writing clean, concise, and efficient code.

Sem comentários: