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 ListPeopleCommand : ICommand
{
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 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");
}
}
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 UpdatePersonCommand : ICommand
{
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");
}
}
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");
}
}
}