Command
An object to instruct a specific action with all required information.
Motivation
Actions and assignments can't be serialized generally, a Command pattern solves by:
- provides a way to store information of actions.
- can undo base on the kept command information.
- can be serialized, logged.
It's heavily used by cli implementations and GUI development.
ICommand
A simple command implementation is an ICommand
interface + an object can be applied on by commands.
A Command should be a immutable object which we will represent it as a record.
var editor = new Editor(content: "hello");
// wrap command info inside a instance
var commandInsert = new EditorCommand(editor, EditorCommandType.Insert, ", world");
var commandDelete = new EditorCommand(editor, EditorCommandType.Delete, 7);
commandInsert.Execute();
commandDelete.Execute();
public interface ICommand
{
void Execute();
}
public record class EditorCommand(Editor Editor, EditorCommandType CommandType, params object[]? Args) : ICommand
{
public void Execute()
{
switch (CommandType)
{
case EditorCommandType.Insert:
Editor.Insert((string)(Args?[0] ?? string.Empty));
break;
case EditorCommandType.Delete:
Editor.Delete((int)(Args?[0] ?? 0));
break;
default:
break;
}
}
public enum EditorCommandType
{
Insert, Delete
}
}
public class Editor
{
private string _content = string.Empty;
public Editor(string content) => _content = content;
public void Insert(string content)
{
_content += content;
Console.WriteLine(_content);
}
public void Delete(int count)
{
_content = _content[..(_content.Length - count)];
Console.WriteLine(_content);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Undo a Command
var editor = new Editor(content: "hello");
var commandInsert = new EditorCommand(editor, EditorCommandType.Insert, ", world");
var commandDelete = new EditorCommand(editor, EditorCommandType.Delete, 7);
commandInsert.Execute();
commandDelete.Execute();
commandDelete.Undo();
commandInsert.Undo();
public interface ICommand
{
void Execute();
void Undo();
}
public enum EditorCommandType
{
Insert, Delete
}
public record class EditorCommand(Editor Editor, EditorCommandType CommandType, params object[]? Args) : ICommand
{
public int InsertLenght { get; init; } = Args?[0] is string s ? s.Length : 0;
private bool succeed; // implying last execute succeed or not
public void Execute()
{
switch (CommandType)
{
case EditorCommandType.Insert:
Editor.Insert((string)(Args?[0] ?? string.Empty));
succeed = true;
break;
case EditorCommandType.Delete:
succeed = Editor.Delete((int)(Args?[0] ?? 0));
break;
default:
break;
}
}
public void Undo()
{
if (!succeed) return; // if previous execution failed, no need to undo
switch (CommandType)
{
case EditorCommandType.Insert:
Editor.Delete(InsertLenght);
break;
case EditorCommandType.Delete:
Editor.Insert(Editor.Deleted);
break;
default:
break;
}
}
}
public class Editor
{
private string _content = string.Empty;
public string Deleted { get; set; } = string.Empty;
public Editor(string content) => _content = content;
public void Insert(string content)
{
_content += content;
Console.WriteLine(_content);
}
public bool Delete(int count)
{
if (_content.Length >= count)
{
Deleted = _content[^count..];
_content = _content[..(_content.Length - count)];
Console.WriteLine(_content);
return true;
}
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
Composite Command
A composite command is the combination of Command pattern and Composite pattern.
- Collection like to store commands with order.
- Execute as chaining, be aware to handle exceptions.
- Undo as chaining, be aware to handle exceptions.
TIP
Commands might also need context to perform actions one by one.
A base class can be like the following.
- A composite command should be
ICommand
too. - A composite command should be a collection like command.
- Default
Execute
can revoke all executed when any command failed.
using System.Runtime.InteropServices;
public interface ICommand
{
void Execute();
void Undo();
bool Success { get; set; }
}
public abstract class CompositeCommand<T> : List<T>, ICommand where T : class?, ICommand
{
public bool Success
{
get => this.All(cmd => cmd.Success);
set => field = value;
}
public virtual void Execute()
{
foreach (var cmd in this)
{
cmd.Execute();
// if any cmd failed, revoke all executed
if (!cmd.Success)
{
var reverse = CollectionsMarshal.AsSpan(this)[..IndexOf(cmd)];
reverse.Reverse();
foreach (var c in reverse)
c.Undo();
return;
}
}
Success = true;
}
public virtual void Undo()
{
foreach (var cmd in Enumerable.Reverse(this))
// only undo executed
if (cmd.Success) cmd.Undo();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Then we can try a deletion with count that exceeds the current content length, which should rollback the content to initial hello
.
var editor = new Editor(content: "hello");
var commandInsert = new EditorCommand(editor, EditorCommandType.Insert, ", world");
var commandDelete = new EditorCommand(editor, EditorCommandType.Delete, 1);
// deletion exceeds the length. will revoke all executions.
var wrongCommand = new EditorCommand(editor, EditorCommandType.Delete, 100);
// should edit back to `hello`
var combined = new EditorCompositeCommand() { commandInsert, commandDelete, wrongCommand };
combined.Execute();
// default implementations are just fine for this.
class EditorCompositeCommand : CompositeCommand<EditorCommand>;
2
3
4
5
6
7
8
9
10
11
12
13
14
TIP
Macros in vim can be implemented using composite commands.