Undo,Redoの実装って何十回もやってる気がする

undo,redoの実装って何十回もやってる気がする。毎回同じパターンだ。undo,redoが登場するような編集ソフトは大体同じパターンに落とせる。フレームワークも作った。ブログにそういう内容を書きたいが面倒くさい。需要があれば面倒でも書くんだけどなあ

http://twitter.com/youpychan/status/994486992

という発言をしたら何人か反応を頂いたので書いてみることにする。

需要があるなら書こう。undo,redoだけじゃなくてグラフィカルな編集ソフト全般の話をいつかまとめたいと思っていたので、ちょいとシリーズで書いてみようかとおもう

http://twitter.com/youpychan/status/994636764

書こうと思う。


まずUndo,Redoについて。


Unod,Redoってみなさんどういう風に実装しているでしょうか?
私はコマンドパターンを使ってコマンドの中に、UndoとRedoの操作に必要な情報を保持するという方法をとっていました。
実際にUndo,Redo処理をするのはコマンドを受け取る側で処理を行います。(何十回のうちの初期のころは割とこのパターン)


コードを使って説明しましょう。

あるデータモデル(DataModelクラスとします)があってsetText(string text)メソッドによってTextプロパティを更新するという例を取り上げます。
まず、コマンドはICommandというインタフェースを持ち、コマンドの実行はICommandインタフェースを通して統一的に行います。

public interface ICommand {
    public CommandType getCommandType();
    public DataModel getDataModel();
}


コマンドの種類を識別するためにCommandTypeという列挙型を用意して、コマンドがそれを返すというのと、操作の対象となるDataModelクラスのオブジェクトを返すというインタフェースを持ちます。


Textを更新するというUPDATE操作をコマンドとして実装します。

public class UpdateCommand implements ICommand {
    private DataModel mModel;
    private String mPrevText;
    private String mNewText;

    public UpdateCommand(DataModel model, String prevText, String newText) {
        mModel = model;
        mPrevText = prevText;
        mNewText = newText;
    }

    public CommandType getCommandType() {
        return CommandType.UPDATE;
    }

    public DataModel getDataModel() {
        return mModel;
    } 

    public String getPrevText() {
        return mPrevText;
    }

    public String getNewText() {
        return mNewText;
    }
}

コマンドの実行部はこんな感じです。

public class CommandInvoker {
    public void undo(ICommand command) {
        switch (command.getCommandType()) {
             case CommandType.UPDATE:
                 UpdateCommand updateCmd = (UpdateCommand)command;
                 updateCmd.getDataModel().setText(updateCmd.getPrevText());
                 break;
             ...
        }
    }
}

undo()メソッドだけを掲載しますが、redo()や最初のinvoke()も同様な実装になることは想像がつくとおもいます。


twitterでは、いつも同じパターンと発言しましたが、実は何十回も実装しているうちに実装が変わってきます。
ここでは、スイッチ文で処理を分岐してキャストしてコマンドオブジェクトを利用しています。オブジェクト指向に慣れ親しんでいるみなさんならもうお気づきでしょうが、これはポリモーフィズムを使って解決すべき問題です。


ポリモーフィズムを使った場合ICommandは以下のようになります。

public interface ICommand {
    public void invoke();
    public void undo();
    public void redo();
}


そしてUpdateCommandの実装は以下の通りです。

public class UpdateCommand implements ICommand {
    private DataModel mModel;
    private String mPrevText;
    private String mNewText;

    public UpdateCommand(DataModel model, String newText) {
        mModel = model;
        mPrevText = prevText;
        mNewText = newText;
    }

    public void invoke() {
        mPrevText = mModel.getText();
        mModel.setText(mNewText);
    }

    public void undo() {
        mModel.setText(mPrevText);
    }

    public void redo() {
        mModel.setText(mNewText);
    }
}

コマンドを処理する側はICommandインタフェースのinvoke(),undo(),redo()を呼び出すだけです。


C#(というかクロージャ)を使うとさらに簡単になります。


もはやICommandインタフェースは必要ありません。以下のように、デリゲートを使用してCommandクラスを定義します。

public class Command
{
     Action Invoke;
     Action Undo;
     Action Redo;
}


コマンドの生成は以下の通りです。

public class CommandInvoker
{
     public ICommand createUpdateCommand(DataModel model, string newText)
     {
          string prevText = null;
          Command cmd = new Command();
          cmd.Invoke = () =>
          {
               prevText = model.Text;
               model.Text = newText;
          };
 
          cmd.Updo = () =>
          {
               model.Text = prevText;
          };

          cmd.Redo = () =>
          {
               model.Text = newText;
          };

          return cmd;
     }
}

Javaでも無名クラスを使えば同様のことが可能です。しかし無名クラスのオブジェクトに値を渡すためにfinal指定しなければならなかったりするのでお勧め出来ない方法です。
クロージャの仕組みの便利さに改めて感心した一例でした。



さて、これではUndo,Redoを実装したことにはなりませんので、次回はコマンドを実行する部分について説明したいと思います。