Airbrake Blog

Behavioral Design Patterns: Command

Written by Frances Banks | Aug 1, 2017 6:00:33 PM

Not to be rude, but I hereby command you to check out today's article in our ongoing Guide to Software Design Patterns series, in which we dive into the extremely useful command design pattern in all its glory! As another behavioral pattern, the command design pattern makes it easy to add work to a queue, which can be processed by a client at any moment in time. Furthermore, the client need not be aware of the underlying functionality or implementation of the queued work.

In this article we'll closely examine the command design pattern, exploring a real world example of the pattern in action, along with a fully-functional C# code sample intended to illustrate how the pattern can be used in your own coding endeavors. Let's get this party started!

In the Real World

To explain a real world example of the command pattern, we should first outline the basic components that make up the bulk of its logic.

  • Receiver - Receives actions via Commands.
  • Command - Binds an action to a Receiver.
  • Invoker - Handles a collection of Commands and determines when Commands are executed.
  • Client - Manages interactions between Receiver / Command and Command / Invoker.

While that's all very high level and abstract, it may begin to make sense when you consider that the postal service as a modern, real world example of the command pattern in action. A recipient waiting to get some mail is a receiver. Letters would be considered commands, each awaiting their time to be "executed," by being delivered to the appropriate recipient. A mailman/mailwoman is obviously the invoker, handling the collection of letters (commands) and determining when they are delivered (executed). The postoffice itself is, therefore, the client, as it determines which letters (commands) are assigned to which mailpersons (invokers).

Moreover, the command pattern is intended to make it easy to revert or undo a previous command action, if necessary. As we all know, this also applies to postal deliveries, since a refused letter or package can easily be picked up and returned to sender, thereby reverting the initial command action.

How It Works In Code

As with other behavioral pattern code samples, the command pattern example is somewhat complex at first glance. We'll completely break down our sample below, so you can easily understand everything that's going on in the code. As an avid gamer myself, I decided to implement a basic Character modification system for our command design pattern example. Here's the basic rundown of the logic:

  • Character class - Our receiver, which contains a few standard statistics (Agility, Charisma, and Strength) that we want to manipulate.
  • Modification class - Our command class, which allows us to modify a statistic for the associated Character instance.
  • ModificationManager class - Our invoker class, which holds the Modifications queue and determines when queued commands should be invoked.
  • Program.Main(string[]) method - Our simple client class, which creates two Characters, a number of Modifications, then proceeds with executing and reverting them to see how Character statistics change.

That's the big picture of what we're going for here, so now we'll start with the full code sample for easy copy-pasting. Afterward, we'll start digging into it to see exactly what's going on:

// <Statistics>/Statistic.cs
namespace Command.Statistics
{
internal enum StatisticType
{
Agility,
Charisma,
Strength
}

internal interface IStatistic
{
decimal Value { get; set; }
}

internal class Strength : IStatistic
{
public decimal Value { get; set; } = 0;
}

internal class Agility : IStatistic
{
public decimal Value { get; set; } = 0;
}

internal class Charisma : IStatistic
{
public decimal Value { get; set; } = 0;
}
}

// Character.cs
using Command.Statistics;

namespace Command
{
/// <summary>
/// Stores basic character information, including statistics.
///
/// Acts as 'Receiver' within Command pattern.
/// </summary>
internal class Character
{
public string Name { get; set; }
public Agility Agility { get; set; } = new Agility();
public Charisma Charisma { get; set; } = new Charisma();
public Strength Strength { get; set; } = new Strength();

public Character(string name)
{
Name = name;
}

public Character(string name, decimal agility, decimal charisma, decimal strength)
{
Name = name;

Agility.Value = agility;
Charisma.Value = charisma;
Strength.Value = strength;
}

public override string ToString()
{
return Name;
}
}
}

// Modification.cs
using System;
using System.Reflection;
using Command.Statistics;
using Utility;

namespace Command
{
internal enum Status
{
ExecuteFailed,
ExecuteSucceeded,
Queued,
RevertFailed,
RevertSucceeded
}

/// <summary>
/// Defines all the fundamental properties and methods of modifications.
///
/// Acts as 'Command' within Command pattern.
/// </summary>
internal interface IModification
{
void Execute();
Guid Id { get; set; }
void Revert();
Status Status { get; set; }
}

/// <summary>
/// Base Modification class, used to alter Character Statistic values.
///
/// Acts as a 'ConcreteCommand' within Command pattern.
/// </summary>
internal class Modification : IModification
{
private readonly Character _character;
private readonly StatisticType _statisticType;

public Guid Id { get; set; } = Guid.NewGuid();
public Status Status { get; set; } = Status.Queued;
public readonly decimal Value;

/// <summary>
/// Get character statistic object.
/// </summary>
internal IStatistic CharacterStatistic => (IStatistic)
_character
.GetType()
.GetProperty(_statisticType.ToString())?
.GetValue(_character);

/// <summary>
/// Get character statistic value property.
/// </summary>
internal PropertyInfo CharacterStatisticValueProperty =>
CharacterStatistic?.GetType().GetProperty("Value");

public Modification(Character character, StatisticType statisticType, decimal value)
{
_character = character;
_statisticType = statisticType;
Value = value;
}

/// <summary>
/// Execute this modification.
/// </summary>
public void Execute()
{
Status = UpdateValue() ? Status.ExecuteSucceeded : Status.ExecuteFailed;

// Output message.
Logging.Log($"{Status} for modification {this}.");
}

/// <summary>
/// Revert this modification.
/// </summary>
public void Revert()
{
Status = UpdateValue(true) ? Status.RevertSucceeded : Status.RevertFailed;

// Output message.
Logging.Log($"{Status} for modification {this}.");
}

/// <summary>
/// Updates the value of the underlying Character Statistic property.
/// </summary>
/// <param name="isReversion">Indicates if this is a reversion command.</param>
/// <returns>Indicates if update was successful.</returns>
internal bool UpdateValue(bool isReversion = false)
{
try
{
// Return if property not set.
if (CharacterStatisticValueProperty == null) return false;

// Assign original and new values.
var originalValue = CharacterStatistic.Value;
var newValue = 0m;
// Add values normally, but subtract if reversion.
newValue = isReversion ? CharacterStatistic.Value - Value : CharacterStatistic.Value + Value;

// Set modified value.
CharacterStatisticValueProperty.SetValue(CharacterStatistic, newValue);

// Output confirmation message.
Logging.Log($"[{_character}] - '{CharacterStatistic.GetType().Name}' {(isReversion ? "reverted" : "modified")} from {originalValue} to {newValue}.");
}
catch (Exception)
{
return false;
}
// Return successful result.
return true;
}

public override string ToString()
{
return $"[Id: {Id}, Statistic: {_statisticType}, Value: {Value}]";
}
}
}

// ModificationManager.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Command
{
/// <summary>
/// Manages the modification queue and actions.
///
/// Acts as 'Invoker' within Command pattern.
/// </summary>
internal class ModificationManager
{
private readonly List<IModification> _queue = new List<IModification>();

/// <summary>
/// Checks if any modifications are queued.
/// </summary>
public bool HasQueue => _queue.Any(x =>
x.Status == Status.Queued ||
x.Status == Status.ExecuteFailed ||
x.Status == Status.RevertFailed);

/// <summary>
/// Add modification to queue.
/// </summary>
/// <param name="modification"></param>
public void AddModification(IModification modification)
{
_queue.Add(modification);
}

/// <summary>
/// Process all outstanding modifications.
/// </summary>
public void ProcessQueue()
{
// Execute modifications that are queued or failed.
foreach (var modification in _queue.Where(x =>
x.Status == Status.Queued ||
x.Status == Status.ExecuteFailed))
{
modification.Execute();
}

// Revert modifications that failed.
foreach (var modification in _queue.Where(x =>
x.Status == Status.RevertFailed))
{
modification.Revert();
}
}

/// <summary>
/// Revert passed modification, if found in queue.
/// </summary>
/// <param name="modification">Modification to revert.</param>
public void RevertModification(IModification modification)
{
// Find match.
var match = _queue.FirstOrDefault(x => x == modification);

// Can't revert a modification not in the queue.
if (match == null)
{
throw new ArgumentException($"Modification [{modification}] not found, cannot revert.");
}

// Can't revert unless execution already took place.
if (match.Status != Status.ExecuteSucceeded)
{
throw new ArgumentException($"Modification [{modification}] 'Status' must be Status.ExecuteSucceeded to revert.");
}

// Revert modification.
match.Revert();

// Update status and remove from queue.
if (match.Status == Status.RevertSucceeded)
{
_queue.Remove(match);
}
}

/// <summary>
/// Get modification by Id and pass to primary RevertModification method.
/// </summary>
/// <param name="id">Id of modification to revert.</param>
public void RevertModification(Guid id)
{
RevertModification(_queue.FirstOrDefault(x => x.Id == id));
}
}
}

// Program.cs
using Command.Statistics;
using Xunit;

namespace Command
{
class Program
{
static void Main(string[] args)
{
// Create a manager.
var manager = new ModificationManager();

// Create a character with initial stats.
var alice = new Character("Alice", 10, 14, 12);
// Create another character with default stats.
var bob = new Character("Bob");

// Create some modifications for Alice.
var agilityAlice = new Modification(alice, StatisticType.Agility, 8);
var charismaAlice = new Modification(alice, StatisticType.Charisma, -4);
var strengthAlice = new Modification(alice, StatisticType.Strength, 0.75m);

// Create modifications for Bob.
var agilityBob = new Modification(bob, StatisticType.Agility, 99.99m);
var charismaBob = new Modification(bob, StatisticType.Charisma, -42);

// Add modifications to queue.
manager.AddModification(agilityAlice);
manager.AddModification(strengthAlice);
manager.AddModification(agilityBob);
manager.AddModification(charismaBob);
manager.AddModification(charismaAlice);

// Process queue.
manager.ProcessQueue();

// Revert agility modification.
manager.RevertModification(agilityAlice);

// Confirm that we can revert in any order, regardless of queue order.
Assert.Equal(bob.Charisma.Value, charismaBob.Value);
manager.RevertModification(charismaBob);
Assert.Equal(bob.Charisma.Value, 0);

// Confirm that passing by Id also works.
Assert.Equal(alice.Strength.Value, 12 + strengthAlice.Value);
manager.RevertModification(strengthAlice.Id);
Assert.Equal(alice.Strength.Value, 12);
}
}
}

// <Utility>/Logging.cs
using System;
using System.Diagnostics;

namespace Utility
{
/// <summary>
/// Houses all logging methods for various debug outputs.
/// </summary>
public static class Logging
{
/// <summary>
/// Determines type of output to be generated.
/// </summary>
public enum OutputType
{
/// <summary>
/// Default output.
/// </summary>
Default,
/// <summary>
/// Output includes timestamp prefix.
/// </summary>
Timestamp
}

/// <summary>
/// Outputs to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="outputType">Output type.</param>
public static void Log(string value, OutputType outputType = OutputType.Default)
{
Debug.WriteLine(outputType == OutputType.Timestamp
? $"[{StopwatchProxy.Instance.Stopwatch.Elapsed}] {value}"
: value);
}
}
}

The Command.Statistics namespace contains just a few statistic classes (Strength, Agility, and Charisma), each of which merely contain a Value property. They're not a fundamental part of the command pattern, so we won't go into anymore detail on those. Instead, let's take a look at the Character class:

// Character.cs
using Command.Statistics;

namespace Command
{
/// <summary>
/// Stores basic character information, including statistics.
///
/// Acts as 'Receiver' within Command pattern.
/// </summary>
internal class Character
{
public string Name { get; set; }
public Agility Agility { get; set; } = new Agility();
public Charisma Charisma { get; set; } = new Charisma();
public Strength Strength { get; set; } = new Strength();

public Character(string name)
{
Name = name;
}

public Character(string name, decimal agility, decimal charisma, decimal strength)
{
Name = name;

Agility.Value = agility;
Charisma.Value = charisma;
Strength.Value = strength;
}

public override string ToString()
{
return Name;
}
}
}

This is our core Receiver class, which is basically the object we'll be acting upon via Modifications. As you can see, each Character just has a Name and a set of statistic properties, each of which have Value properties of their own that default to 0.

Next, we move onto the Modification class. However, before we do so, let's look at the IModification interface that Modification implements:

// Modification.cs
using System;
using System.Reflection;
using Command.Statistics;
using Utility;

namespace Command
{
internal enum Status
{
ExecuteFailed,
ExecuteSucceeded,
Queued,
RevertFailed,
RevertSucceeded
}

/// <summary>
/// Defines all the fundamental properties and methods of modifications.
///
/// Acts as 'Command' within Command pattern.
/// </summary>
internal interface IModification
{
void Execute();
Guid Id { get; set; }
void Revert();
Status Status { get; set; }
}

// ...
}

One of the goals of our command pattern example is the ability to revert modifications that have already been made. This effectively allows the system to freely perform any modifications, in any order, and even to roll them back in any order as well, without confusing the logic or incorrectly modifying the underlying Character statistic values. To that end, we'll be using the Command.Status enumeration values throughout our logic to assign and check the current stage of each Modification.

The IModification interface is really the actual Command object we'll be using. However, since it's an interface, we refer to classes that implement said interface as ConcreteCommands. The fundamental methods we need are Execute() to trigger an action and Revert() to roll an action back. We also store the Status and unique Id, in case we need to refer to a Modification elsewhere in code.

Now we get to the Modification class, which is ultimately a Command, although we could have multiple Command classes to implement the IModification interface, so calling Modification a ConcreteCommand is a bit more accurate in this context.

/// <summary>
/// Base Modification class, used to alter Character Statistic values.
///
/// Acts as a 'ConcreteCommand' within Command pattern.
/// </summary>
internal class Modification : IModification
{
private readonly Character _character;
private readonly StatisticType _statisticType;

public Guid Id { get; set; } = Guid.NewGuid();
public Status Status { get; set; } = Status.Queued;
public readonly decimal Value;

/// <summary>
/// Get character statistic object.
/// </summary>
internal IStatistic CharacterStatistic => (IStatistic)
_character
.GetType()
.GetProperty(_statisticType.ToString())?
.GetValue(_character);

/// <summary>
/// Get character statistic value property.
/// </summary>
internal PropertyInfo CharacterStatisticValueProperty =>
CharacterStatistic?.GetType().GetProperty("Value");

public Modification(Character character, StatisticType statisticType, decimal value)
{
_character = character;
_statisticType = statisticType;
Value = value;
}

/// <summary>
/// Execute this modification.
/// </summary>
public void Execute()
{
Status = UpdateValue() ? Status.ExecuteSucceeded : Status.ExecuteFailed;

// Output message.
Logging.Log($"{Status} for modification {this}.");
}

/// <summary>
/// Revert this modification.
/// </summary>
public void Revert()
{
Status = UpdateValue(true) ? Status.RevertSucceeded : Status.RevertFailed;

// Output message.
Logging.Log($"{Status} for modification {this}.");
}

/// <summary>
/// Updates the value of the underlying Character Statistic property.
/// </summary>
/// <param name="isReversion">Indicates if this is a reversion command.</param>
/// <returns>Indicates if update was successful.</returns>
internal bool UpdateValue(bool isReversion = false)
{
try
{
// Return if property not set.
if (CharacterStatisticValueProperty == null) return false;

// Assign original and new values.
var originalValue = CharacterStatistic.Value;
var newValue = 0m;
// Add values normally, but subtract if reversion.
newValue = isReversion ? CharacterStatistic.Value - Value : CharacterStatistic.Value + Value;

// Set modified value.
CharacterStatisticValueProperty.SetValue(CharacterStatistic, newValue);

// Output confirmation message.
Logging.Log($"[{_character}] - '{CharacterStatistic.GetType().Name}' {(isReversion ? "reverted" : "modified")} from {originalValue} to {newValue}.");
}
catch (Exception)
{
return false;
}
// Return successful result.
return true;
}

public override string ToString()
{
return $"[Id: {Id}, Statistic: {_statisticType}, Value: {Value}]";
}
}

The Modification constructor method accepts a Character, a StatisticType (which is just an easy way to refer to Agility, Charisma, or Strength statistics), and a decimal value. The value parameter represents the potential change in value for the passed StatisticType associated with the passed Character instance. Fundamentally, that's all that a Modification does -- it changes one of the statistic property values of the passed Character.

This value change is performed in the Execute() method, which passes most of its logical behavior to the UpdateValue(bool) method. This method uses the CharacterStatisticValueProperty and CharacterStatistic properties -- both of which use a bit of reflection magic to obtain their underlying represented object values -- to calculate and update that new underlying Value. The bool isReversion parameter allows us to use UpdateValue(bool) for both Execute() and Revert() calls, with almost no logical difference between the two methods.

Next let's look at the ModificationManager, which is our invoker class. It handles the Modification queue and determines when Modifications need to be executed, reverted, or ignored:

// ModificationManager.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Command
{
/// <summary>
/// Manages the modification queue and actions.
///
/// Acts as 'Invoker' within Command pattern.
/// </summary>
internal class ModificationManager
{
private readonly List<IModification> _queue = new List<IModification>();

/// <summary>
/// Checks if any modifications are queued.
/// </summary>
public bool HasQueue => _queue.Any(x =>
x.Status == Status.Queued ||
x.Status == Status.ExecuteFailed ||
x.Status == Status.RevertFailed);

/// <summary>
/// Add modification to queue.
/// </summary>
/// <param name="modification"></param>
public void AddModification(IModification modification)
{
_queue.Add(modification);
}

/// <summary>
/// Process all outstanding modifications.
/// </summary>
public void ProcessQueue()
{
// Execute modifications that are queued or failed.
foreach (var modification in _queue.Where(x =>
x.Status == Status.Queued ||
x.Status == Status.ExecuteFailed))
{
modification.Execute();
}

// Revert modifications that failed.
foreach (var modification in _queue.Where(x =>
x.Status == Status.RevertFailed))
{
modification.Revert();
}
}

/// <summary>
/// Revert passed modification, if found in queue.
/// </summary>
/// <param name="modification">Modification to revert.</param>
public void RevertModification(IModification modification)
{
// Find match.
var match = _queue.FirstOrDefault(x => x == modification);

// Can't revert a modification not in the queue.
if (match == null)
{
throw new ArgumentException($"Modification [{modification}] not found, cannot revert.");
}

// Can't revert unless execution already took place.
if (match.Status != Status.ExecuteSucceeded)
{
throw new ArgumentException($"Modification [{modification}] 'Status' must be Status.ExecuteSucceeded to revert.");
}

// Revert modification.
match.Revert();

// Update status and remove from queue.
if (match.Status == Status.RevertSucceeded)
{
_queue.Remove(match);
}
}

/// <summary>
/// Get modification by Id and pass to primary RevertModification method.
/// </summary>
/// <param name="id">Id of modification to revert.</param>
public void RevertModification(Guid id)
{
RevertModification(_queue.FirstOrDefault(x => x.Id == id));
}
}
}

The most important component of the ModificationManager is the List<IModification> _queue property. This collection is used by every method within the class. For example, the HasQueue property uses LINQ to check if any queued objects have either a failing Status or are Queued (indicating they haven't been processed yet).

The AddModification(IModification) method allows us to add Modifications to the queue. Similarly, RevertModification(IModification) attempts to find the passed Modification in the queue. If it's found and has recently been successfully executed, the Revert() method is called on that Modification, which rolls back the value of the Character statistic it's associated with. The RevertModification(Guid) overload allows us to perform reversion based on Id instead of an actual Modification instance argument.

Lastly, the ProcessQueue() method will be used to perform the majority of logical processing of the queue. This can be safely executed at any time, and simply finds any queued objects that recently failed or have never been executed. This is why a distinction between the Status.RevertFailed and Status.ExecuteFailed enumeration values is important, since we can use the Status value to determine which action to take on the failed Modification.

Lastly, we get to actually using our command pattern in some way, which is where the Program.Main(string[]) method comes in. This is effectively our client object:

// Program.cs
using Command.Statistics;
using Xunit;

namespace Command
{
class Program
{
static void Main(string[] args)
{
// Create a manager.
var manager = new ModificationManager();

// Create a character with initial stats.
var alice = new Character("Alice", 10, 14, 12);
// Create another character with default stats.
var bob = new Character("Bob");

// Create some modifications for Alice.
var agilityAlice = new Modification(alice, StatisticType.Agility, 8);
var charismaAlice = new Modification(alice, StatisticType.Charisma, -4);
var strengthAlice = new Modification(alice, StatisticType.Strength, 0.75m);

// Create modifications for Bob.
var agilityBob = new Modification(bob, StatisticType.Agility, 99.99m);
var charismaBob = new Modification(bob, StatisticType.Charisma, -42);

// Add modifications to queue.
manager.AddModification(agilityAlice);
manager.AddModification(strengthAlice);
manager.AddModification(agilityBob);
manager.AddModification(charismaBob);
manager.AddModification(charismaAlice);

// Process queue.
manager.ProcessQueue();

// Revert agility modification.
manager.RevertModification(agilityAlice);

// Confirm that we can revert in any order, regardless of queue order.
Assert.Equal(bob.Charisma.Value, charismaBob.Value);
manager.RevertModification(charismaBob);
Assert.Equal(bob.Charisma.Value, 0);

// Confirm that passing by Id also works.
Assert.Equal(alice.Strength.Value, 12 + strengthAlice.Value);
manager.RevertModification(strengthAlice.Id);
Assert.Equal(alice.Strength.Value, 12);
}
}
}

Nothing too crazy going on here. We start by creating a ModificationManager instance, along with a couple Characters, and a number of Modification instances for assorted statistics and values. Then, we add the Modifications, in an irrelevant order, to the manager queue, before calling manager.ProcessQueue() to apply all the modifications. The output, up to this point, shows everything working as expected:

[Alice] - 'Agility' modified from 10 to 18.
ExecuteSucceeded for modification [Id: 6a65df10-db5e-49aa-9aa3-abe3110af714, Statistic: Agility, Value: 8].

[Alice] - 'Strength' modified from 12 to 12.75.
ExecuteSucceeded for modification [Id: 4a926ef9-68a8-4f76-a10d-0541c74cf150, Statistic: Strength, Value: 0.75].

[Bob] - 'Agility' modified from 0 to 99.99.
ExecuteSucceeded for modification [Id: 0c7f10d5-d0b2-41f7-9042-caf162a79b54, Statistic: Agility, Value: 99.99].

[Bob] - 'Charisma' modified from 0 to -42.
ExecuteSucceeded for modification [Id: fada200a-fb2b-4b0d-8067-47460c5de055, Statistic: Charisma, Value: -42].

[Alice] - 'Charisma' modified from 14 to 10.
ExecuteSucceeded for modification [Id: 18f42f5e-f8be-419b-b2cb-21185db4c08b, Statistic: Charisma, Value: -4].

Now that the queue contains some Modifications we can revert any of these changes we need to, at any time. In this case, our code does so right away, but our assertions verify that the appropriate statistic Values are reverting as expected. We also confirm that we can call RevertModification(Guid) using an Id rather than a Modification instance still results in proper behavior. Sure enough, the output (and passing tests) confirm everything is working:

[Alice] - 'Agility' reverted from 18 to 10.
RevertSucceeded for modification [Id: 6a65df10-db5e-49aa-9aa3-abe3110af714, Statistic: Agility, Value: 8].
[Bob] - 'Charisma' reverted from -42 to 0.
RevertSucceeded for modification [Id: fada200a-fb2b-4b0d-8067-47460c5de055, Statistic: Charisma, Value: -42].
[Alice] - 'Strength' reverted from 12.75 to 12.00.
RevertSucceeded for modification [Id: 4a926ef9-68a8-4f76-a10d-0541c74cf150, Statistic: Strength, Value: 0.75].

So there you have it! A small but relatively robust example of the command design pattern in action. For more information on all the other popular design patterns, head on over to our ongoing design pattern series here!