コマンドパターンは結構万能
メメントパターンを再現できる
コマンドパターンは「処理対象に適用する処理」をオブジェクトとして定義し、それを配列に記録して管理するものです。
以下のように定義すれば、メメントパターンも再現することができます。
public class MementoCommand<T> : ICommand<T>
{
T before, applied;
public MementoCommand(T before,T applied)
{
this.before = before;
this.applied = applied;
}
public void Apply(ref T target)
{
target = applied;
}
public void Undo(ref T target)
{
target = before;
}
}
使い方
// 処理する対象と、ヒストリーの初期化
T target = new();
CommandHistory<T> history = new();
// 何かのコマンドをターゲットに適用(自分で定義してね)
SomeCommand someCommand = new(a,b,c);
someCommand.Apply(ref target);
// 適用したコマンドをヒストリーに登録
history.NewCommand(someCommand);
// Undo(操作取り消し)
history.Undo(ref target);
// Redo(再実行)はUndoと同じ感じ
リングバッファで実装してみた
調べてみるとRedoスタックとUndoスタックを使った実装ばっかりで登場人物が多く、私好みではありませんでした。リングバッファでシンプルに書いてみました。
using System;
public interface ICommand<T>
{
// redo undoできない処理は、結果そのものをコマンドに入れる
public void Apply(ref T target);
public void Undo(ref T target);
}
public class CommandHistory<T>
{
const int MaxSize = 8;
// appliedCommandCountは[0,MaxSize]の間を変化する
// appliedCommandCount = activeCountのとき,最新(もうRedoできない)
// appliedCommandCount = 0 のとき,適用したコマンドはない(もうUndoできない)
int appliedCommandCount = 0;
ICommand<T>[] commands = new ICommand<T>[MaxSize];
int ringStart;
int activeCount;
void GetActiveCommand(int index, out ICommand<T> command)
{
if (index < activeCount) command = commands[(index + ringStart) % MaxSize];
else throw new IndexOutOfRangeException();
}
public bool Undo(ref T target)
{
// 適用したコマンドの数がNなら、N番[N-1]に登録された処理をUndoする。
if (appliedCommandCount > 0)
{
GetActiveCommand(--appliedCommandCount, out var command);
command.Undo(ref target);
return true;
}
else if (appliedCommandCount == 0)
{
return false;
}
else throw new Exception("CommandHistoryで、Undo時に不正な状態が発生しました。適用したコマンドの数はマイナスになってはいけません。");
}
public bool Redo(ref T target)
{
// 適用したコマンドの数が0なら、1番[0]に登録されたコマンドを適用
// 適用したコマンドの数がNなら、N+1番[N]に登録されたコマンドを適用
if (appliedCommandCount < activeCount)
{
GetActiveCommand(appliedCommandCount++, out var command);
command.Apply(ref target);
return true;
}
else if (appliedCommandCount == activeCount)
{
return false;
}
else throw new Exception("CommandHistoryで、Redo時に不正な状態が発生しました。適用したコマンドの数はredo可能サイズを超えてはいけません。");
}
public void NewCommand<CommandT>(in CommandT command) where CommandT : ICommand<T>
{
// appliedCommandCountが,0の時も,MaxSizeの時も、[ringStart]を更新する
commands[(appliedCommandCount + ringStart) % MaxSize] = command;
if (appliedCommandCount < MaxSize)//パンパンじゃないとき
{
activeCount = ++appliedCommandCount;
}
else//パンパンの時
{
ringStart = (ringStart + 1) % MaxSize;
}
}
}
注意
このコードはChatGPTに添削してもらうと、NewCommandの部分で「activeCountが更新されていません」とか言われます。多分elseの時の話なんでしょうが、更新する必要がないので更新してないだけ。
他にも何か言われますが、AIに聞いただけでコメントするのはやめてください。自分の頭で考えたコメントはどんなのでも歓迎します。
テストコード
class Program
{
static void Main()
{
CommandHistory<int> intHistory = new();
intHistory.NewCommand(new AddCommand(1));
intHistory.NewCommand(new AddCommand(2));
intHistory.NewCommand(new AddCommand(4));
intHistory.NewCommand(new AddCommand(8));
intHistory.NewCommand(new AddCommand(16));
intHistory.NewCommand(new AddCommand(32));
intHistory.NewCommand(new AddCommand(64));
intHistory.NewCommand(new AddCommand(128));
intHistory.NewCommand(new AddCommand(256));
int n = 511;
while (true)
{
Console.WriteLine("コマンドを入力してね a z y e");
var r = Console.ReadKey();
if (r.KeyChar == 'z')
{
if (intHistory.Undo(ref n))
{
Console.WriteLine($"undoしました。数値→{n}");
}
else Console.WriteLine("undoはもうできません");
}
else if (r.KeyChar == 'y')
{
if (intHistory.Redo(ref n))
{
Console.WriteLine($"redoしました。数値→{n}");
}
else Console.WriteLine("redoできません");
}
else if (r.KeyChar == 'a')
{
intHistory.NewCommand(new AddCommand(1));
Console.WriteLine($"新しいコマンドを追加しました。数値→{++n}");
}
else if (r.KeyChar == 'e') break;
}
}
}
public class AddCommand : ICommand<int>
{
public int num;
public AddCommand(int num) => this.num = num;
public void Apply(ref int target)
{
target += num;
}
public void Undo(ref int target)
{
target -= num;
}
}
最後に
このコードは全て必要最低限の実装なので、特殊なケース、たとえば「外部の参照を持っているTへの処理はバグるんじゃない?」みたいなものについては一切考慮していません。そういうのは臨機応変に対応してください。