Command Pattern

Use Case: GUI Commands, multi undo/redo, need to serialize sequence of actions/calls. There are many use cases!

Code examples below from Dmitri Nesteruk

Definition

The command pattern lets you build an object which represents an instruction to perform a particular action. This command contains all the information needed for the action to be taken.

Command Example

Run a batch of commands on a Bank Account, these commands could then be serialized and persisted to a data store.

  1. Create BankAccount with its methods access modifiers set to internal, this means they are accessible only within files in the same assembly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class BankAccount
{
private int _balance;
private int overdraftLimit = -500;

public BankAccount(int balance = 0)
{
_balance = balance;
}

internal void Deposit(int amount)
{
_balance += amount;
WriteLine($"Deposited ${amount}, balance is now {_balance}");
}

internal bool Withdraw(int amount)
{
if (_balance - amount >= overdraftLimit)
{
_balance -= amount;
WriteLine($"Withdrew ${amount}, balance is now {_balance}");
return true;
}
return false;
}

public override string ToString()
{
return $"{nameof(_balance)}: {_balance}";
}
}
  1. Create our command interface and its implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public interface ICommand
{
void Call();
void Undo();
}

public class BankaccountCommand : ICommand
{
public enum Action
{
Deposit, Withdraw
}

private BankAccount _account;
private Action _action;
private int _amount;
private bool _succeeded;

public Bank_accountCommand(BankAccount account, Action action, int amount)
{
_account = account ?? throw new ArgumentNullException(paramName: nameof(_account));
_action = action;
_amount = amount;
}

public void Call()
{
switch (_action)
{
case Action.Deposit:
_account.Deposit(_amount);
succeeded = true;
break;
case Action.Withdraw:
_succeeded = _account.Withdraw(_amount);
break;
default:
throw new ArgumentOutOfRangeException();
}
}

// assumes `Deposit` is the opposite of `Withdraw`
public void Undo()
{
if (!_succeeded) return;

switch (_action)
{
case Action.Deposit:
_account.Withdraw(_amount);
break;
case Action.Withdraw:
_account.Deposit(_amount);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
  1. Use the commands
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var bankAccount = new BankAccount();
var commands = new List<BankAccountCommand>
{
new BankAccountCommand(bankAccount, BankAccountCommand.Action.Deposit, 100),
new BankAccountCommand(bankAccount, BankAccountCommand.Action.Withdraw, 1000)
};

WriteLine(bankAccount);

foreach (var c in commands)
c.Call();

WriteLine(bankAccount);

foreach (var c in Enumerable.Reverse(commands))
c.Undo();

WriteLine(bankAccount);

Composite Command Example

This is a combination of the Composite Pattern and the Command pattern. Building on the example above a Composite Command could be used to transfer money from account A to account B. This will wrap several elements into one element which has the same API.

  1. Extend the interface to include a Succcess property.
1
2
3
4
5
6
public interface ICommand
{
void Call();
void Undo();
bool Success { get; set; }
}
  1. Create a general purpose composite command
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CompositeBankAccountCommand : List<BankAccountCommand>, ICommand
{
public CompositeBankAccountCommand(IEnumerable<BankAccountCommand> collection) : base(collection)
{

}

public void Call()
{
ForEach(cmd => cmd.Call());
}

public void Undo()
{
foreach (var cmd in
((IEnumerable<BankAccountCommand>)this).Reverse())
{
if (cmd.Success) cmd.Undo();
}
}

public bool Success
{
get
{
// A composite command is successful only if all of the constituent parts succeed
return this.All(cmd => cmd.Success);
}
set
{
// Sets each `Success` - not sure if this is the best approach
foreach(var cmd in this)
cmd.Success = value;
}
}
}
  1. Use the command to test it.
1
2
3
4
5
6
7
8
9
10
11
var bankAccount = new BankAccount();
var deposit = new BankAccountCommand(bankAccount, BankAccountCommand.Action.Deposit, 100);
var withdraw = new BankAccountCommand(bankAccount, BankAccountCommand.Action.Withdraw, 50);

var composite = new CompositeBankAccountCommand(new []{ deposit, withdraw });

composite.Call();
WriteLine(bankAccount);

composite.Undo();
WriteLine(bankAccount);
  1. Now to do the money transfer command we need to change how Call works as we need to care that the subsequent commands succeeded. This can be accomplished by making the Call and Undo methods virtual which allows the implementation to be overridden.
1
2
3
public virtual void Call() { }
public virtual void Undo() { }
public virtual bool Success { }
  1. Create type MoneyTransferCommand

This is the implementation of a composite command using infrastructure from the above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MoneyTransferCommand : CompositeBankAccountCommand 
{
public MoneyTransferCommand (BankAccount from, BankAccount to, int amount)
{
// We inherit from a list in the base class so we can call its `AddRange` method
AddRange(new [] {
new BankAccountCommand(from, BankAccountCommand.Action.Withdraw, amount),
new BankAccountCommand(to, BankAccountCommand.Action.Deposit, amount)
});
}

// We need to override the base call command so we have consistency
public override void Call()
{
// we dont want to call a subsequent command if the previous command failed

// this will keep a reference to the last command we envoked
BankAccountCommand last = null;
foreach(var cmd in this)
{
// precondition is there is no previous command or the previous command succeeded
if (last == null || last.Success)
{
cmd.Call();
last = cmd;
}
else
{
cmd.Undo();
break; // the whole chain has failed
}
}
}
}
  1. Use the composite command (happy path)
1
2
3
4
5
6
7
8
var from = new BankAccount(100);
var to = new BankAccount();

var mtc = new CompositeBankAccountCommand(from, to, 100);
mtc.Call();

WriteLine(from);
WriteLine(to);
  1. Try transfer more than the balance allows. The overridden call has checks for this so will Undo the command and break out of the commands.
1
var mtc = new CompositeBankAccountCommand(from, to, 1000);

References