0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Undo]リングバッファでコマンドパターン

Last updated at Posted at 2025-09-30

コマンドパターンは結構万能

メメントパターンを再現できる

コマンドパターンは「処理対象に適用する処理」をオブジェクトとして定義し、それを配列に記録して管理するものです。

以下のように定義すれば、メメントパターンも再現することができます。

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への処理はバグるんじゃない?」みたいなものについては一切考慮していません。そういうのは臨機応変に対応してください。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?