8

I've created a rollback/retry mechanism using the Command Pattern and a custom retry mechanism with the RetryCount and timeout threshold properties as input (As described here Which is the best way to add a retry/rollback mechanism for sync/async tasks in C#?). I also tried the Polly framework instead and it is great!

Now, I want to wrap them in an abstraction. I could describe it as commands plan mechanism or command based decision mechanism.

So, Based on the different combinations of command results I have to decide which of the accomplished commands will be reverted and which of them not (I want to offer the ability to press a ReTry button and some off the commands shouldn't be reverted).

These commands are grouped in some categories and I have to take different rollback strategy for the different groups and commands (based on the result). Some kind of different policies would take place here!

IMPORTANT: I want to avoid IF/ELSE etc.Maybe the Chain-of-responsibility pattern would help me but I DON'T know and I really want help:

//Pseudo-code...

CommandHandler actionManager1 = new Action1Manager();
CommandHandler actionManager2 = new Action2Manager();
CommandHandler actionManager2 = new Action3Manager();

actionManager1.SetNextObjManager(Action2Manager);
actionManager2.SetNextObjManager(Action3Manager);

actionManager1.ProcessAction(){...}
actionManager1.ProcessAction(){...}
...

Another idea could be observables or event handlers but the question is how to use them (?). Or just by using a list/stack/queue of Commands and check that list to take the next-step decision (?):

  private List<ICommand> rollbackCommandsOfAllGroups;
  private List<ICommand> rollbackCommandsOfGroup1;
  private List<ICommand> rollbackCommandsOfGroup2;
...

SCENARIOS

Finally, You can think of twenty (20) commands grouped in four (4) categories.

Scenario 1

  • Group 1 Commands (5) were successful
  • Group 2 Commands 1 and 2 were successful but Command 3 was FAILED.Command 4 and 5 not executed
  • Group 3 not executed
  • Group 4 not executed

Decision 1

  • Rollback Commands 1 and 2 from Group 2 BUT not the whole Group 1 Commands

Scenario 2

  • Group 1 Commands (5) were successful
  • Group 2 Commands (5) were successful
  • Group 3 Commands (5) were successful
  • Group 4 Commands 1 - 4 were successful BUT Command 5 was FAILED

Decision 2

  • Rollback All Commands from all Groups

I want to guide me and help me with CODE examples in C#.

Community
  • 1
  • 1
Giannis Grivas
  • 3,374
  • 1
  • 18
  • 38
  • in scenario 2 should it be `Group 3 Commands(5)` instead of two times `Group 2`? – wake-0 Jun 03 '16 at 11:43
  • Yeah @KevinWallis its three (3) – Giannis Grivas Jun 03 '16 at 11:48
  • I would suggest to steer discussion about how to model not only commands, but also the nature of dependencies between them. Provided scenarios shows just outcome, but do not describe what are the factors contribute to the rollback decision. I think that could have an impact of what would be the right solution, what other readers think? – Edgars Pivovarenoks Jun 04 '16 at 05:21
  • @Edgars I'd like to hear some ideas about how to structure a decision web app based on some command parts and their results.I only presented how the commands are implemented and I want to take it further. This is the point of my question and I am defenitely going to raise a bounty on this to motivate expierenced devs to help me with that.I want to know how others would think in that case with code examples.Thanks btw – Giannis Grivas Jun 04 '16 at 09:21
  • 1
    @GiannisGrivas let's laser focus in "Command 5" in Scenario 2, can you elaborate what special could be about that particular command, what makes it different from others that it invalidates other commands even across other groups? – Edgars Pivovarenoks Jun 05 '16 at 05:55
  • @Edgars yes it has a different meaning. This command can be an update to a configuration file (added xml elements from all other actions,their results) at the host.And for this reason ,for example, we want to rollback the whole process. Something like "fatal error". I want to kwow how can I build such as a flexible mechanism to catch these scenarios. – Giannis Grivas Jun 05 '16 at 08:28
  • @GiannisGrivas, I think we are getting there. Ok, back to Scenarion 1 - "Command 3" in "Group 2", is it also kind of important command relative to others? Also confirm : in Scenarion 1 Group 1 is not rolled back? – Edgars Pivovarenoks Jun 05 '16 at 08:53
  • @Edgars Yeah Command 3 from group has a higher importance BUT this is the case where a command has an importance only for its group! Which means that Group1 Commands (1-5) will stay without rollback in that case. There will be a retry button afterwards. As you can see there can be a lot of commands combinations.This is the reason that I want the most flexible solution posible.Thanks again for your time! – Giannis Grivas Jun 05 '16 at 09:06
  • [1] Currently I see two types of command rollbacks - a. group, b, whole batch. Now, is it possible to have scenario that there are partially rollbacked/partially committed groups for ex., failed command : GRP5(CMD3) and commit only GRP1(CMD1-2), GRP2(CMD2) .. etc.? [2] Just to confirm, will combinations be assembled during run-time? [3] Will you receive whole batch in one go, or commands could be added during execution with some kind of commit/save at the end? – Edgars Pivovarenoks Jun 06 '16 at 04:13

1 Answers1

9

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

Johnny Svarog
  • 1,125
  • 10
  • 17
  • you forgot to implement the BatchRollback method. Can you fix that.Also can you give me a working example project from the code you wrote as it has build errors? – Giannis Grivas Jun 05 '16 at 10:22
  • Can you add a full implementation? – Giannis Grivas Jun 05 '16 at 13:55
  • I've created a GitHub repository for this example. There is a UnitTestProject with all classes from my answer here: [link](https://github.com/judarical/Commands-pattern-example). – Johnny Svarog Jun 05 '16 at 17:02
  • Actually, I haven't implemented the BatchRollback in MultiCommand class, because in those scenarios it wasn't necesarry. So, you'll find 'throw new NotImplementedException();' line in the class. – Johnny Svarog Jun 05 '16 at 17:05
  • Please, open the project in VIsual Studio and run tests in Tests.cs file one by one to execute those two scenarios. Those tests will write information to file. See 'Output' function in BaseCommand.cs to find where the file is situated or change that function as you like. – Johnny Svarog Jun 05 '16 at 17:13
  • @Jonny Svarog what do you suggest about how to check the commands groups ever time and decide what strategy to follow. Go to next step/ Proceed to rollback / Set command as completed /Terminate the whole process. Do you thin a check error method in every command would be enough or do I have to check observables or notifypropertychanged? Thank you in advance! – Giannis Grivas Jun 05 '16 at 17:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/113859/discussion-between-giannis-grivas-and-johnny-svarog). – Giannis Grivas Jun 05 '16 at 18:51