Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a strategy decision mechanism based on the results combinations of multi-commands in C#

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#.

like image 271
Giannis Grivas Avatar asked Jun 03 '16 11:06

Giannis Grivas


1 Answers

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

like image 123
Johnny Svarog Avatar answered Oct 18 '22 13:10

Johnny Svarog