I guess, you need something like MultiCommand Pattern. It is defined very well in HeadFirst Design Patterns book.
I've created a draft design, which allows to manage commands in flexible manner. That's not the final decision, but just a rough idea which might be used for further design... So, here we go:
First, we'll create an interface for all commands:
interface ICommand
{
bool Execute();
void Rollback();
void BatchRollback();
}
Rollback() action will cancel the command itself, while BatchRollback() will cancel all the stack of dependent commands.
Second, we will create an abstract BaseCommand class with simple Execute() and Rollback() methods implementation:
abstract class BaseCommand : ICommand
{
private ICommand rollbackMultiCommand;
#region constructors
protected BaseCommand(MultiCommand rollbackMultiCommand = null)
{
this.rollbackMultiCommand = rollbackMultiCommand;
}
#endregion
protected abstract bool ExecuteAction();
public abstract void Rollback();
public bool Execute()
{
if (!ExecuteAction())
{
BatchRollback();
return false;
}
return true;
}
public void BatchRollback()
{
Rollback();
if (rollbackMultiCommand != null)
rollbackMultiCommand.Rollback();
}
}
Note, that we've used a Template Method pattern too: Execute() method of the base class implements base logic for command execution and rollback,
while the specific action of each command will be implemented in child classes' ExecuteAction() method.
Please, look at BaseCommand constructor: it accepts MultiCommand class as a parameter, which stores the list of commands to rollback if
something goes wrong during execution.
Here is MultiCommand class implementation:
class MultiCommand : ICommand
{
private List<ICommand> rollbackCommands;
private List<ICommand> commands;
public MultiCommand(List<ICommand> commands, List<ICommand> rollbackCommands)
{
this.rollbackCommands = rollbackCommands;
if (rollbackCommands != null)
this.rollbackCommands.Reverse();
this.commands = commands;
}
#region not implemented members
// here other not implemented members of ICommand
#endregion
public bool Execute()
{
foreach (var command in commands)
{
if (!command.Execute())
return false;
}
return true;
}
public void Rollback()
{
foreach (var rollbackCommand in rollbackCommands)
{
rollbackCommand.Rollback();
}
}
#endregion
}
Well, our commands will look like that:
class Command1 : BaseCommand
{
public Command1(MultiCommand rollbackMultiCommand = null) : base(rollbackMultiCommand)
{
}
protected override bool ExecuteAction()
{
Output("command 1 executed");
return true;
}
public override void Rollback()
{
Output("command 1 canceled");
}
}
For simplicity, I've implemented Command2, Command3, Command4 the same way as Command1. Then, for failed command simulation I've created this one:
class FailCommand : BaseCommand
{
public FailCommand(MultiCommand rollbackMultiCommand = null) : base(rollbackMultiCommand)
{
}
protected override bool ExecuteAction()
{
Output("failed command executed");
return false;
}
public override void Rollback()
{
Output("failed command cancelled");
}
}
Let's try to use all this staff:
Scenario 1:
[TestMethod]
public void TestCommands()
{
var command1 = new Command1();
var command2 = new Command2();
var command3 = new Command3(new MultiCommand(null, new List<ICommand> { command1 }));
var command4 = new FailCommand(new MultiCommand(null, new List<ICommand> { command1, command2, command3 }));
var group1 = new MultiCommand(new List<ICommand>
{
command1,
command2
}, null);
var group2 = new MultiCommand(new List<ICommand>
{
command3,
command4
}, null);
var groups = new MultiCommand(new List<ICommand>
{
group1,
group2
}, null);
groups.Execute();
}
As you can see, we created some commands, pushed them into groups (groups are Multicommands, which can have as much nesting as you like).
Then, we pushed group1 and group2 into variable group to use all commands like one. We also pushed into constructor of Command3 and FailCommand the
list of commands which should be rollbacked if something goes wrong with them. In the example, our command3 executes well, but command4 fails.
So, we expect command4, command3, command2, command1 to cancel after fail.
Here is the output of the test:
command 1 executed
command 2 executed
command 3 executed
failed command executed
failed command cancelled
command 3 canceled
command 2 canceled
command 1 canceled
Scenario 2
Almost the same as scenario 1, but here we want to rollback only command3 and command1, if command3 fails:
[TestMethod]
public void TestCommands2()
{
var command1 = new Command1();
var command2 = new Command2();
var command3 = new FailCommand(new MultiCommand(null, new List<ICommand> { command1 }));
var command4 = new Command4(new MultiCommand(null, new List<ICommand> { command1, command2, command3 }));
var group1 = new MultiCommand(new List<ICommand>
{
command1,
command2
}, null);
var group2 = new MultiCommand(new List<ICommand>
{
command3,
command4
}, null);
var groups = new MultiCommand(new List<ICommand>
{
group1,
group2
}, null);
groups.Execute();
}
Output is here:
command 1 executed
command 2 executed
failed command executed
failed command cancelled
command 1 canceled
I hope it will be useful in your situation...
You can find this example on GitHub.
In recent commits you can find more flexible version of this approach