ダイアログ編集にMementoパターン

先日のエントリーのコメントにてMementoパターン使用のご提案を頂きましたので、考えてみました。

まず、Mementoに気づかず実装した、現在のコード。

  1. 編集ダイアログのコンストラクタでモデルのコピー(workspaceModel)を作成し、それに対して編集を行う。
  2. 編集結果は、getWorkspaceModel() で取得する。
  3. OKだった場合、オリジナルモデルにworkspaceModelを反映させるコマンド(EditTableCommand)を発行する。
  4. Cancelだった場合、何もしない。
public class TableEditPart extendsimplements … {

  public void doubleClicked() {
    // オリジナルモデルを取得
    final TableModel tableModel = (TableModel) getModel();
    final DatabaseModel dbModel = (DatabaseModel) getParent().getModel();

    // ダイアログを生成
    final TableEditDialog dialog = new TableEditDialog(
        getViewer().getControl().getShell(), tableModel, dbModel);

    // 実行
    if(dialog.open() == Dialog.OK) {
      // OK だった場合
      final GraphicalViewer viewer = (GraphicalViewer) getViewer();

      // テーブル編集コマンドを発行
      viewer.getEditDomain().getCommandStack().execute(
          new EditTableCommand(tableModel, dialog.getWorkspaceModel()));
    }
  }
}

public class TableEditDialog extends Dialog {

  /** 編集用のモデルコピー */
  private TableModel workspaceModel;

  public TableEditDialog(
      final Shell parentShell,
      final TableModel tableModel,
      final DatabaseModel dbModel) {

    // 略

    // モデルのコピーを取得する。
    workspaceModel = (TableModel) tableModel.getWorkspaceModel();
  }

  // 略 (各種編集はworkspaceModelに対して行う)

  /** 編集結果を取得する */
  public TableModel getWorkspaceModel() {
    return workspaceModel;
  }
}

public class EditTableCommand extends Command {

  /** 編集対象のモデル */
  private final TableModel targetTableModel;

  /** 編集前のコピーモデル */
  private final TableModel oldTableWorkspaceModel;

  /** 編集後のコピーモデル */
  private final TableModel newTableWorkspaceModel;

  public EditTableCommand(
      final TableModel targetTableModel,
      final TableModel newTableWorkspaceModel) {

    super();
    this.targetTableModel = targetTableModel;

    // 編集前の状況をコピーとして保存
    this.oldTableWorkspaceModel = (TableModel) targetTableModel.getWorkspaceModel();

    this.newTableWorkspaceModel = newTableWorkspaceModel;
  }

  @Override
  public void execute() {
    // 編集後の状況をターゲットに反映
    targetTableModel.setWorkspaceModel(newTableWorkspaceModel);
  }

  @Override
  public void undo() {
    // 編集前の状況をターゲットに反映
    targetTableModel.setWorkspaceModel(oldTableWorkspaceModel);
  }
}

では、これをMementoパターン版にしてみよう。

さて、これをMementoパターン版に変えてみました。

  1. 事前に編集前のスナップショットを保存する。
  2. 編集ダイアログでは、オリジナルモデルを直接編集する。
  3. 編集後のスナップショットを保存する。
  4. OKだった場合、EditTableCommandを発行する。(※)
  5. Cancelだった場合、編集前のスナップショットを利用して、オリジナルモデルを元に戻す。
public class TableEditPart extendsimplements … {

  public void doubleClicked() {
    // オリジナルモデルを取得
    final TableModel tableModel = (TableModel) getModel();
    final DatabaseModel dbModel = (DatabaseModel) getParent().getModel();

    // 編集前のスナップショットを保存
    final TableModel beforeEditMemento = tableModel.createMemento();

    // ダイアログを生成
    final TableEditDialog dialog = new TableEditDialog(
        getViewer().getControl().getShell(), tableModel, dbModel);

    // 実行
    if(dialog.open() == Dialog.OK) { // OK だった場合
      // 編集後のスナップショットを保存
      final TableModel afterEditMemento = tableModel.createMemento();
      final GraphicalViewer viewer = (GraphicalViewer) getViewer();

      // テーブル編集コマンドを発行
      viewer.getEditDomain().getCommandStack().execute(
          new EditTableCommand(tableModel, beforeEditMemento, afterEditMemento));
    } else { // Cancel だった場合
      // 編集前にロールバック
      tableModel.setMemento(beforeEditMemento);
    }
  }
}

public class TableEditDialog extends Dialog {

  /** オリジナルのモデル */
  private TableModel tableModel;

  public TableEditDialog(
      final Shell parentShell,
      final TableModel tableModel,
      final DatabaseModel dbModel) {

    // 略

    this.tableModel = tableModel;
  }

  // 略 (各種編集はオリジナルのモデルに対して直接行う)
}

public class EditTableCommand extends Command {

  private final TableModel targetTableModel;
  private final TableModel beforeEditMemento;
  private final TableModel afterEditMemento;
    
  public EditTableCommand(
      final TableModel targetTableModel,
      final TableModel beforeEditMemento,
      final TableModel afterEditMemento) {
    
    super();
    this.targetTableModel = targetTableModel;
    this.beforeEditMemento = beforeEditMemento;
    this.afterEditMemento = afterEditMemento;
  }
  
  @Override
  public void execute() {
    // 編集後のスナップショットをオリジナルに反映
    targetTableModel.setMemento(afterEditMemento);
  }

  @Override
  public void undo() {
    // 編集前のスナップショットをオリジナルに反映
    targetTableModel.setMemento(beforeEditMemento);
  }
}

こんな感じでしょうか。

ただ、気になるのは上記手順(※)でマークした部分。

ダイアログ実行後、既にオリジナルモデルには変更が反映されています。(オリジナルを直接編集しているので、当たり前ですね。)従って、EditTableCommand を実行する必要は無い訳です。が、テーブルの編集をUNDO/REDOする為に、コマンド発行が必要になってしまいます。

というわけで、UNDO/REDOでは無い、初回編集時にも、execute() が実行され、あまり意味の無いことをしてしまいます。

このままでも問題はないのでしょうけど…。

では、どうすれば効率が良くなるだろうか。

Command クラスを覗いてみました。

public abstract class Command {

  public void execute() { }

  public void undo() { }

  public void redo() {
    execute();
  }

  // 略
}

おー。redo() ってのがあるじゃないですか。コイツをオーバーライドしてしまえば良いんですね。
execute() はオーバーライドしなければ、何もしませんので、放置。

public class EditTableCommand extends Command {

  private final TableModel targetTableModel;
  private final TableModel beforeEditMemento;
  private final TableModel afterEditMemento;
    
  public EditTableCommand(
      final TableModel targetTableModel,
      final TableModel beforeEditMemento,
      final TableModel afterEditMemento) {
    
    super();
    this.targetTableModel = targetTableModel;
    this.beforeEditMemento = beforeEditMemento;
    this.afterEditMemento = afterEditMemento;
  }

  @Override
  public void undo() {
    // 編集前のスナップショットをオリジナルに反映
    targetTableModel.setMemento(beforeEditMemento);
  }

  @Override
  public void redo() {
    // 編集後のスナップショットをオリジナルに反映
    targetTableModel.setMemento(afterEditMemento);
  }
}

これで、スマートになったでしょうか。