Monday 8 October 2018

Command Pattern

The command (Action, Transaction) pattern is used to decouple logic; it allows you to encapsulate your "Action" within an object that your main client can call to execute. Something to keep in mind is that your command is entirely self sufficient, it really should rely on itself, with no arguments being passed from the calling object to the execute.

Commands can have execute and unexecute definitions, thus facilitating undo functionality. let's take a look at a simplified UML representation, the UML i'm using is tailored to my example.


now our invoker maintains a stack of commands that have been fired, this is then used by the undo method to undo whatever it is our command did. Our commands that can be undone are sent to our invoker via the ExecuteCommand method and of course we set our ListPeopleCommand through our constructor; the ListPeopleCommand is an exception, because it can't really be undone.

next let's focus on our ICommand interface it's concrete class and their relationships with the invoker; the ICommand interface simply defines an execute and an unexecute method, one does something, the other undoes that thing. it's really up to us to come up with the logic for both. Our concrete implementation of the ICommand Interface has a reference to a receiver, basically the thing our commands will be modifying. you can notice that it's only the concrete command class that knows anything about the receiver, and this facilitates our decoupling between the invoker and receiver.

First let's take a look at this person class

class Person
{
    static int runningId = 0;
    public int Id { get; private set; } = runningId++;
    public string Name { get; set; }
    public Person(string name) : base() => Name = name;
    public string Display() => $"{Id}) {Name}";

}

Not  exactly a brain buster, next let's take a look at our ICommand interface.

interface ICommand
{
    void Execute();
    void Unexecute();

}

Again nothing to it, just like the UML says two defined methods, next let's make things a bit interesting and take a look at our invoker class

class Invoker {
    Stack<ICommand> commandStack = new Stack<ICommand>();
    ICommand listPeopleCommand;

    public Invoker(ICommand listPeopleCommand) => this.listPeopleCommand = listPeopleCommand;

    public void ExecuteCommand(ICommand command) {
        commandStack.Push(command);
        commandStack.Peek().Execute();
    }

    public void ListPeople() => listPeopleCommand.Execute();

    public void Undo() {
        if (commandStack.Count > 0)
            commandStack.Pop().Unexecute();
        else
            Console.WriteLine("Nothing to undo");
    }

}

notice that we still haven't even mentioned a concrete implementation of our ICommand interface, the invoker wants ICommands, but is indifferent to their inner workings other than implementing the ICommand interface, nor does the invoker know or care about the receiver. Pay attention to the ExecuteCommand method, it pushes an ICommand onto our stack and executes the command, then our undo method does the opposite, pops an ICommand and calls it's unexecute method.

Next let's take a look at the ListPeopleCommand

class ListPeopleCommandICommand
{
    IList<Person> people;
    public ListPeopleCommand(IList<Person> People) => people = People;

    public void Execute()
    {
        Console.Clear();
        foreach (var person in people)
            Console.WriteLine($"\t{person.Display()}");
        Console.WriteLine();
    }

    public void Unexecute() { //do nothing; }

}

we looked at the ListPeopleCommand first because it doesn't require an unexecute implementation, but notice that it takes in a receiver, in this case a list of people.

Now let's take a look at the CreatePersonCommand

class CreatePersonCommandICommand
{
    IList<Person> people;
    Person person;
    public CreatePersonCommand(IList<Person> People) => people = People;

    public void Execute()
    {
        Console.WriteLine("\nEnter in the persons name");
        string name = null;
        while (String.IsNullOrEmpty(name = Console.ReadLine()));

        person = new Person(name);
        people.Add(person);
        Console.Clear();
        Console.WriteLine($"\tCreated:{person.Display()}");
    }

    public void Unexecute()
    {
        people.Remove(person);
        Console.WriteLine($"\tUndone:{person.Display()} creation");
    }

}

Again our command takes in a reference to our receiver, notice that our command maintains a reference to the person we create outside of the Execute method's scope, this is so that our unexecute method can remove that person from our people list.

next lets' create UpdatePersonCommand, and DeletePersonCommand.

class UpdatePersonCommandICommand
{
    IList<Person> people;
    string oldName;
    Person person;
  
    public UpdatePersonCommand(IList<Person> People) => people = People;

    public void Execute()
    {
        Console.WriteLine("\nEnter in the id of the person who's name you want to update");
        int personId = -1;
        while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out personId));

        Console.WriteLine("\nEnter in the new name of the person to update");
        string newName = null;

        while (String.IsNullOrEmpty(newName = Console.ReadLine()));

        person= people.FirstOrDefault(p => p.Id == personId);
        oldName = person.Name;
        Console.Clear();
        Console.Write($"{person.Display()} to ");

        person.Name = newName;
        Console.WriteLine(person.Name);
    }

    public void Unexecute()
    {
        Console.Write($"undone rename {person.Name} ");
        person.Name = oldName;
        Console.WriteLine($"back to {oldName}");
    }
}

class DeletePersonCommandICommand
{
    IList<Person> people;

    Person person;
    public DeletePersonCommand(IList<Person> People) => people = People;

    public void Execute()
    {
        Console.WriteLine("\nEnter in the id of the person who you want to delete");

        int personId = -1;
        while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out personId));

        person = people.FirstOrDefault(p => p.Id == personId);
        Console.Clear();
        Console.WriteLine($"\tRemoved:{person.Display()}\r\n");

        people.Remove(person);
    }

    public void Unexecute()
    {
        people.Add(person);
        Console.WriteLine($"undone delete: added person {person.Display()} back");
    }

}

they both follow a similar pattern as our CreatePersonCommand just with varying logic. next lets take a look at our main

class Program
{
    static void Main(string[] args)
    {
        var selection = -1;
        var people = new List<Person>(); /* Receiver */
        var invoker = new Invoker(new ListPeopleCommand(people));

        do
        {
            switch (selection)
            {
                case 1:
                    invoker.ExecuteCommand(new CreatePersonCommand(people));
                    break;
                case 2:
                    invoker.ExecuteCommand(new UpdatePersonCommand(people));
                    break;
                case 3:
                    invoker.ExecuteCommand(new DeletePersonCommand(people));
                    break;
                case 4:
                    invoker.ListPeople();
                    break;
                case 9:
                    invoker.Undo();
                    break;
            }

            ListCommands();

            while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out selection));

        } while (selection > 0);

    }

    static void ListCommands()
    {
        Console.WriteLine("1) Add person");
        Console.WriteLine("2) Update person");
        Console.WriteLine("3) Delete person");
        Console.WriteLine("4) List people");
        Console.WriteLine("9) undo");
        Console.WriteLine("0) Exit");
    }

}

as you can see it's actually pretty straight forward, pay attention to the instantiation of our invoker, we pass in our ListPeopleCommand, then in our application logic we simply call the defined ListPeople method defined in our Invoker, same goes for the Undo Method, just calling it on the invoker. the neat thing is the undo-able commands where we pass new instances of them to the Invoker via the ExecuteCommand(command:ICommand) method.

with this approach we can easily execute commands and undo them.

a quick caveat is that the app itself is very brittle there's almost no preventative measures in place so if you try to delete or edit an item that's not there well the app will crash.

here's the full code from start to finish.

using System;
using System.Collections.Generic;
using System.Linq;

namespace pav.commandPattern
{
    class Person
    {
        static int runningId = 0;
        public int Id { get; set; } = runningId++;
        public string Name { get; set; }
        public Person(string name) : base() => Name = name;
        public string Display() => $"{Id}) {Name}";
    }

    interface ICommand
    {
        void Execute();
        void Unexecute();
    }

    class CreatePersonCommand : ICommand
    {
        IList<Person> people;
        Person person;

        public CreatePersonCommand(IList<Person> People) => people = People;

        public void Execute()
        {
            Console.WriteLine("\nEnter in the persons name");
            string name = null;
            while (String.IsNullOrEmpty(name = Console.ReadLine()));
            person = new Person(name);
            people.Add(person);
            Console.Clear();
            Console.WriteLine($"\tCreated:{person.Display()}");
        }

        public void Unexecute()
        {
            people.Remove(person);
            Console.WriteLine($"\tUndone:{person.Display()} creation");
        }
    }

    class UpdatePersonCommand : ICommand
    {
        //reciever
        IList<Person> people;

        string oldName;
        Person person;

        public UpdatePersonCommand(IList<Person> People) => people = People;

        public void Execute()
        {
            Console.WriteLine("\nEnter in the id of the person who's name you want to update");
            int personId = -1;
            while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out personId));

            Console.WriteLine("\nEnter in the new name of the person to update");
            string newName = null;

            while (String.IsNullOrEmpty(newName = Console.ReadLine()));

            person = people.FirstOrDefault(p => p.Id == personId);
            oldName = person.Name;
            Console.Clear();
            Console.Write($"{person.Display()} to ");

            person.Name = newName;
            Console.WriteLine(person.Name);
        }

        public void Unexecute()
        {
            Console.Write($"undone rename {person.Name} ");
            person.Name = oldName;
            Console.WriteLine($"back to {oldName}");
        }
    }

    class DeletePersonCommand : ICommand
    {
        IList<Person> people;

        Person person;
        public DeletePersonCommand(IList<Person> People) => people = People;

        public void Execute()
        {
            Console.WriteLine("\nEnter in the id of the person who you want to delete");

            int personId = -1;
            while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out personId));

            person = people.FirstOrDefault(p => p.Id == personId);
            Console.Clear();
            Console.WriteLine($"\tRemoved:{person.Display()}\r\n");

            people.Remove(person);
        }

        public void Unexecute()
        {
            people.Add(person);
            Console.WriteLine($"undone delete: added person {person.Display()} back");
        }
    }

    class ListPeopleCommand : ICommand
    {
        IList<Person> people;

        public ListPeopleCommand(IList<Person> People) => people = People;

        public void Execute()
        {
            Console.Clear();
            Console.WriteLine("\nPeople");

            foreach (var person in people)
                Console.WriteLine($"\t{person.Display()}");
            Console.WriteLine();
        }

        public void Unexecute() {/*do nothing*/}
    }

    class Invoker
    {
        Stack<ICommand> commandStack = new Stack<ICommand>();
        ICommand ListPeopleCommand;
        public Invoker(ICommand listPeopleCommand) => ListPeopleCommand = listPeopleCommand;

        public void ExecuteCommand(ICommand command)
        {
            commandStack.Push(command);
            commandStack.Peek().Execute();
        }

        public void ListPeople() => ListPeopleCommand.Execute();

        public void Undo()
        {
            if (commandStack.Count > 0)
                commandStack.Pop().Unexecute();
            else
                Console.WriteLine("Nothing to undo");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var selection = -1;
            var people = new List<Person>(); /* Receiver */
            var invoker = new Invoker(new ListPeopleCommand(people));

            do
            {
                switch (selection)
                {
                    case 1:
                        invoker.ExecuteCommand(new CreatePersonCommand(people));
                        break;
                    case 2:
                        invoker.ExecuteCommand(new UpdatePersonCommand(people));
                        break;
                    case 3:
                        invoker.ExecuteCommand(new DeletePersonCommand(people));
                        break;
                    case 4:
                        invoker.ListPeople();
                        break;
                    case 9:
                        invoker.Undo();
                        break;
                }

                ListCommands();

                while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out selection));

            } while (selection > 0);

        }

        static void ListCommands()
        {
            Console.WriteLine("1) Add person");
            Console.WriteLine("2) Update person");
            Console.WriteLine("3) Delete person");
            Console.WriteLine("4) List people");
            Console.WriteLine("9) undo");
            Console.WriteLine("0) Exit");
        }
    }
}