元に戻すエンジンのデザインパターン


117

土木工学アプリケーション用の構造モデリングツールを書いています。建物全体を表す1つの巨大なモデルクラスがあります。これには、カスタムクラスでもあるノード、線要素、荷重などのコレクションが含まれます。

モデルを変更するたびにディープコピーを保存する元に戻すエンジンをすでにコーディングしています。さて、私は別の方法でコーディングできるかどうか考え始めました。ディープコピーを保存する代わりに、各モディファイアアクションのリストを、対応するリバースモディファイヤとともに保存することもできます。現在のモデルにリバースモディファイヤを適用して元に戻したり、モディファイヤをやり直したりできるようにしました。

オブジェクトのプロパティなどを変更する単純なコマンドをどのように実行するか想像できますが、複雑なコマンドはどうでしょうか。新しいノードオブジェクトをモデルに挿入し、新しいノードへの参照を維持するいくつかの線オブジェクトを追加するようなものです。

それを実装するにはどうすればよいですか?


「元に戻すアルゴリズム」というコメントを追加すると、「元に戻すアルゴリズム」を検索してこれを見つけることができますか?それが私が検索したものであり、重複として閉じられたものを見つけました。
ピーターターナー

私は開発中のアプリケーションで元に戻す/やり直しも開発したいと思います。QT4フレームワークを使用していて、多くの複雑な元に戻す/やり直しアクションが必要です。コマンドパターンを使用して成功しましたか?
Ashika Umanga Umagiliya

2
@umanga:うまくいきましたが、簡単ではありませんでした。最も困難な部分は、参照を追跡することでした。たとえば、Frameオブジェクトが削除されると、その子オブジェクトであるノード、それに作用する荷重、および他の多くのユーザー割り当ては、元に戻すときに再挿入するために保持する必要がありました。しかし、これらの子オブジェクトのいくつかは他のオブジェクトと共有され、元に戻す/やり直しロジックは非常に複雑になりました。モデルがそれほど大きくなかった場合、私は記念品のアプローチを続けます。実装がはるかに簡単です。
Ozgur Ozcitak

これは取り組むべき面白い問題です。svnのように、ソースコードのリポジトリがそれをどのように行うかを考えます(コミット間の差分を保持します)。
アレックス

回答:


88

これまでに見たほとんどの例では、コマンドパターンのバリアントを使用しています。取り消し可能なすべてのユーザーアクションは、アクションを実行してロールバックするためのすべての情報を含む独自のコマンドインスタンスを取得します。その後、実行されたすべてのコマンドのリストを維持し、それらを1つずつロールバックできます。


4
これは基本的に、Cocoaの取り消しエンジンNSUndoManagerがどのように機能するかです。
amrox 2008

33

OPが意味するサイズとスコープのモデルを扱う場合、メモリとコマンドの両方は実用的ではないと思います。それらは機能しますが、維持および拡張することは多くの作業になります。

このタイプの問題については、モデルに関与するすべてのオブジェクトの差分チェックポイントをサポートするために、データモデルのサポートを組み込む必要があると思います。私はこれを一度やったことがあり、それは非常に滑らかに機能しました。あなたがしなければならない最大のことは、モデル内でポインタや参照を直接使用しないことです。

別のオブジェクトへのすべての参照は、何らかの識別子(整数など)を使用します。オブジェクトが必要なときはいつでも、テーブルからオブジェクトの現在の定義を検索します。この表には、以前のバージョンをすべて含む各オブジェクトのリンクリストと、それらがアクティブであったチェックポイントに関する情報が含まれています。

元に戻す/やり直しの実装は簡単です。アクションを実行して新しいチェックポイントを確立します。すべてのオブジェクトバージョンを前のチェックポイントにロールバックします。

コードにはある程度の規律が必要ですが、多くの利点があります。モデルの状態を差分保存しているので、深いコピーは必要ありません。使用するメモリの量(CADモデルなどでは非常に重要)を、使用するREDO数またはメモリのいずれかでスコープできます。元に戻す/やり直しを実装するために何もする必要がないため、モデルで動作する関数の非常にスケーラブルでメンテナンスが簡単です。


1
データベース(例:sqlite)をファイル形式として使用する場合、これはほぼ自動的に実行できます
Martin Beckett

4
モデルへの変更によって導入された依存関係を追跡することでこれを補強する場合、元に戻すツリーシステムが存在する可能性があります(つまり、桁の幅を変更し、別のコンポーネントで作業を行うと、戻って元に戻すことができます)桁は他のものを失うことなく変化します)。そのためのUIは少し扱いに​​くいかもしれませんが、従来の線形の取り消しよりもはるかに強力です。
スムドゥフェルナンド

このIDとポインタのアイデアについて詳しく説明できますか?確かにポインタ/メモリアドレスはIDと同じように機能しますか?
ポール、2014年

@paulm:基本的に、実際のデータは(id、version)によって索引付けされます。ポインターはオブジェクトの特定のバージョンを参照しますが、オブジェクトの現在の状態を参照しようとしているため、(id、version)ではなく、idでアドレスを指定します。(バージョン=>データ)テーブルへのポインターを格納し、毎回最新のものを選択するように再構築することができますが、データを永続化しているときに局所性を損なう傾向があり、泥だらけの懸念が少しあり、困難になりますある種の一般的なクエリを実行するため、通常行われる方法とは異なります。
クリスモーガン

17

GoFについて話している場合、Mementoパターンは特に取り消しに対処します。


7
実際にはそうではありませんが、これは彼の最初のアプローチに対応しています。彼は別のアプローチを求めています。最初のステップは各ステップの完全な状態を保存し、後者は「差分」のみを保存します。
AndreiRînea10年

15

他の人が述べたように、コマンドパターンは元に戻す/やり直しを実装する非常に強力な方法です。ただし、コマンドパターンについては、重要な利点があります。

コマンドパターンを使用して元に戻す/やり直しを実装する場合、データに対して実行される操作を(ある程度)抽象化し、元に戻す/やり直しシステムでそれらの操作を利用することにより、大量の重複コードを回避できます。たとえば、テキストエディタでは、カットアンドペーストは補完的なコマンドです(クリップボードの管理を除く)。つまり、カットの元に戻す操作は貼り付けであり、貼り付けの元に戻す操作はカットされます。これは、テキストの入力や削除など、より単純な操作に適用されます。

ここで重要なのは、元に戻す/やり直しシステムをエディターの主要なコマンドシステムとして使用できることです。「元に戻すオブジェクトの作成、ドキュメントの変更」などのシステムを作成する代わりに、「元に戻すオブジェクトを作成し、元に戻すオブジェクトに対してやり直し操作を実行してドキュメントを変更する」ことができます。

さて、確かに、多くの人々は「まあ、コマンドパターンの要点の一部ではないのですか」と自分自身に考えています。はい。しかし、2つのコマンドセット(1つは即時操作用、もう1つは元に戻す/やり直し用)を持つコマンドシステムが多すぎます。即時操作と元に戻す/やり直しに固有のコマンドがないと言っているわけではありませんが、重複を減らすことでコードの保守性が向上します。


1
私は考えたことがないpasteようcut^ -1。
Lenar Hoyt 2013

8

元に戻すにはPaint.NETコードを参照することをお勧めします-彼らは本当に素晴らしい元に戻すシステムを持っています。おそらく必要なものよりも少し単純ですが、いくつかのアイデアやガイドラインが得られるかもしれません。

-アダム


4
実際、Paint.NETコードは使用できなくなりましたが、フォークされたcode.google.com/p/paint-monoを取得できます
Igor Brejc

7

これは、CSLAが適用される場合です。これは、Windowsフォームアプリケーションのオブジェクトに複雑な取り消しサポートを提供するように設計されています。


6

Mementoパターンを使用して複雑なundoシステムを正常に実装しました。非常に簡単で、自然にRedoフレームワークを提供できるという利点もあります。さらに微妙な利点は、集約されたアクションを1つの元に戻す操作に含めることができることです。

一言で言えば、あなたは記念品オブジェクトの2つのスタックを持っています。1つは元に戻す、もう1つはやり直しです。すべての操作で新しい記念品が作成されます。理想的には、モデル、ドキュメント(またはその他)の状態を変更するための呼び出しになります。これは、元に戻すスタックに追加されます。元に戻す操作を行うときは、Mementoオブジェクトに対して元に戻すアクションを実行してモデルを元に戻すだけでなく、オブジェクトを元に戻すスタックからポップし、それをやり直しスタックにプッシュします。

ドキュメントの状態を変更するメソッドがどのように実装されるかは、実装に完全に依存します。API呼び出し(たとえば、ChangeColour(r、g、b))を簡単に実行できる場合は、その前にクエリを実行して、対応する状態を取得して保存します。しかし、パターンはディープコピーの作成、メモリスナップショット、一時ファイルの作成などもサポートします。これは単に仮想メソッドの実装であるため、すべてあなた次第です。

集約アクション(たとえば、Shiftキーを押しながらオブジェクトのロードを選択して、削除、名前変更、属性の変更などの操作を実行する)を実行するには、コードで新しいUndoスタックを1つのメモリとして作成し、それを実際の操作に渡します。個々の操作を追加します。したがって、アクションメソッドは、(a)心配するグローバルスタックを必要とせず、(b)単独で実行する場合でも、1つの集約操作の一部として実行する場合でも同じようにコーディングできます。

アンドゥシステムの多くはメモリ内のみですが、必要に応じてアンドゥスタックを永続化することもできます。


5

私のアジャイル開発の本でコマンドパターンについて読んだところです-たぶんそれは可能性がありますか?

すべてのコマンドにコマンドインターフェース(Execute()メソッドがある)を実装させることができます。元に戻す場合は、Undoメソッドを追加できます。

詳細はこちら


4

メンデルトシーベンガ一緒ですコマンドパターンを使用する必要があるという事実について、。あなたが使用したパターンはMementoパターンでした。

メモリを大量に消費するアプリケーションで作業しているので、取り消しエンジンが使用できるメモリの量、保存される取り消しのレベルの数、または永続化されるストレージのいずれかを指定できるはずです。これを行わないと、すぐにマシンのメモリー不足が原因でエラーが発生します。

選択したプログラミング言語/フレームワークで元に戻すためのモデルをすでに作成したフレームワークがあるかどうかを確認することをお勧めします。新しいものを発明するのは良いことですが、実際のシナリオですでに書かれ、デバッグされ、テストされているものを取るのが良いです。あなたがこれを書いているものを追加すると、人々は彼らが知っているフレームワークを勧めることができるので役に立ちます。


3

Codeplexプロジェクト

これは、従来のコマンドデザインパターンに基づいて、アプリケーションに元に戻す/やり直し機能を追加するためのシンプルなフレームワークです。アクションのマージ、ネストされたトランザクション、遅延実行(トップレベルのトランザクションコミットでの実行)、および可能な非線形の取り消し履歴(やり直す複数のアクションを選択できる場合)をサポートしています。


2

私が読んだほとんどの例では、コマンドまたはメモリーのパターンを使用しています。しかし、単純なdeque-structureを使用すれば、デザインパターンなしでも実行できます。


両端キューに何を入れますか?

私の場合、元に戻す/やり直し機能が必要な操作の現在の状態を入れました。2つのデック(元に戻す/やり直し)を行うことで、元に戻すキューで元に戻し(最初のアイテムをポップ)、それをやり直しデキューに挿入します。デキュー内のアイテム数が推奨サイズを超える場合、テールのアイテムをポップします。
Patrik Svensson、

2
あなたが実際に記述すると、ISデザインパターン:)。このアプローチの問題は、状態が大量のメモリを消費する場合です-数十の状態バージョンを保持すると、実用的でなくなったり、不可能になったりします。
Igor Brejc、2009年

または、通常の操作と元に戻す操作を表すクロージャーのペアを保存できます。
Xwtek

2

ソフトウェアをマルチユーザーコラボレーションにも適したものにする、元に戻す処理の賢い方法は、データ構造の運用上の変換を実装することです。

この概念はあまり一般的ではありませんが、明確で有用です。定義が抽象的すぎると思われる場合、このプロジェクトは、JSONオブジェクトの運用変換がJavaScriptで定義および実装される方法の成功例です



1

「オブジェクト」のファイルの読み込みと保存のシリアル化コードを再利用して、オブジェクトの状態全体を保存および復元するための便利なフォームを作成しました。これらのシリアル化されたオブジェクトを元に戻すスタックにプッシュします-実行された操作に関するいくつかの情報と、シリアル化されたデータから十分な情報が収集されなかった場合にその操作を元に戻すヒント。元に戻すとやり直しは、しばしば1つのオブジェクトを別のオブジェクトに置き換えるだけです(理論的には)。

奇妙な元に戻すやり直しシーケンスを実行するときに修正されなかったオブジェクトへのポインター(C ++)に起因する多くのバグが多数あります(これらの場所は、安全な元に戻す認識「識別子」に更新されていません)。この領域のバグは、多くの場合...うーん...興味深い。

一部の操作は、サイズ/サイズの移動など、速度/リソース使用量の特殊なケースになる場合があります。

複数選択は、いくつかの興味深い複雑さも提供します。幸い、コードにはグループ化の概念がすでにありました。Kristopher Johnsonがサブアイテムについてコメントするのは、私たちの仕事にかなり近いです。


これは、モデルのサイズが大きくなるにつれて、ますます機能しなくなるように聞こえます。
ウォーレンP

どのように?このアプローチは、新しい「もの」が各オブジェクトに追加されるため、変更なしで機能し続けます。オブジェクトのシリアル化された形式のサイズが大きくなると、パフォーマンスが問題になる可能性がありますが、これは大きな問題ではありませんでした。このシステムは20年以上継続的に開発されており、数千人のユーザーが使用しています。
アードバーク2010

1

ペグジャンプパズルゲームのソルバーを作成するときに、これを行わなければなりませんでした。私は、それぞれの移動を、それが実行または元に戻すことができる十分な情報を保持するCommandオブジェクトを作成しました。私の場合、これは開始位置と各移動の方向を保存するのと同じくらい簡単でした。次に、これらのオブジェクトをすべてスタックに格納し、プログラムがバックトラック中に必要な数の移動を簡単に元に戻すことができるようにしました。


1

Undo / Redoパターンの既成の実装をPostSharpで試すことができます。https://www.postsharp.net/model/undo-redo

自分でパターンを実装しなくても、元に戻す/やり直し機能をアプリケーションに追加できます。Recordableパターンを使用してモデルの変更を追跡し、PostSharpにも実装されているINotifyPropertyChangedパターンと連携します。

UIコントロールが提供され、各操作の名前と粒度を決定できます。


0

コマンドでアプリケーションのモデル(つまり、MFCを使用していたCDocument ...)に加えられたすべての変更が、モデル内に保持されている内部データベースのフィールドを更新することにより、コマンドの最後に保持されるアプリケーションに取り組んだことがありました。したがって、アクションごとに個別の元に戻す/やり直しコードを記述する必要はありませんでした。元に戻すスタックは、レコードが変更されるたびに(各コマンドの最後に)主キー、フィールド名、および古い値を記憶するだけでした。


0

デザインパターンの最初のセクション(GoF、1994)には、元に戻す/やり直しをデザインパターンとして実装するための使用例があります。


0

最初のアイデアを効果的にすることができます。

永続的なデータ構造を使用し、古い状態への参照のリストを維持してください。(ただし、実際に機能するのは、状態クラスのすべてのデータが不変で、そのすべての操作が新しいバージョンを返す場合のみですが、新しいバージョンはディープコピーである必要はありません。変更された部分を置き換えてください 'copy -on-write ')


0

ここでは、コマンドパターンが非常に役立つことがわかりました。いくつかのリバースコマンドを実装する代わりに、APIの2番目のインスタンスで遅延実行を伴うロールバックを使用しています。

このアプローチは、少ない実装労力と簡単な保守性が必要な場合(そして2番目のインスタンスに追加のメモリを割り当てることができる場合)は妥当と思われます。

例については、こちらをご覧くださいhttps : //github.com/thilo20/Undo/


-1

これが役立つかどうかはわかりませんが、私のプロジェクトの1つで同様のことをしなければならなかったとき、UndoEngineをhttp://www.undomadeeasy.comからダウンロードしてしまいました-素晴らしいエンジンボンネットの下にあるものについてはあまり気にしませんでした。


ソリューションを提供する自信がある場合にのみ、回答としてコメントを投稿してください!それ以外の場合は、質問の下にコメントとして投稿することをお勧めします!(今それが許可されていない場合は、良い評判が得られるまでお待ちください)
InfantPro'Aravind '

-1

私の意見では、UNDO / REDOは2つの方法で広く実装できます。1.コマンドレベル(コマンドレベルの取り消し/やり直しと呼ばれる)2.ドキュメントレベル(グローバルの取り消し/やり直しと呼ばれる)

コマンドレベル:多くの回答が指摘するように、これはMementoパターンを使用して効率的に達成されます。コマンドがアクションのジャーナル化もサポートしている場合、やり直しは簡単にサポートされます。

制限:コマンドのスコープがなくなると、元に戻す/やり直しは不可能になり、ドキュメントレベル(グローバル)の元に戻す/やり直しが発生します。

大量のメモリ空間を必要とするモデルに適しているため、あなたのケースはグローバルの元に戻す/やり直しに当てはまると思います。また、これは選択的に元に戻す/やり直しにも適しています。2つのプリミティブタイプがあります。

  1. 全メモリの取り消し/やり直し
  2. オブジェクトレベルのやり直し

「すべてのメモリの元に戻す/やり直し」では、メモリ全体が接続されたデータ(ツリー、リスト、グラフなど)として扱われ、メモリはOSではなくアプリケーションによって管理されます。したがって、C ++の場合、新しい演算子と削除演算子は、aなどの操作を効果的に実装するためのより具体的な構造を含むようにオーバーロードされます。ノードが変更された場合は、b。データの保持やクリアなどの機能は、基本的にメモリ全体をコピーし(メモリ割り当てがすでに高度なアルゴリズムを使用してアプリケーションによって最適化および管理されている場合)、それをスタックに格納することです。メモリのコピーが要求された場合、ツリー構造は、浅いコピーまたは深いコピーを持つ必要性に基づいてコピーされます。ディープコピーは、変更された変数に対してのみ作成されます。すべての変数はカスタム割り当てを使用して割り当てられるため、アプリケーションには、必要に応じていつ削除するかという最終決定権があります。一連の操作をプログラムで選択的に元に戻す/やり直す必要がある場合に、元に戻す/やり直しを分割する必要がある場合は、非常に興味深いことが起こります。この場合、それらの新しい変数、または削除された変数または変更された変数にのみフラグが付けられるため、元に戻す/やり直しはそれらのメモリのみを元に戻す/やり直します。そのような場合、「ビジターパターン」の新しいアイデアが使用されます。「オブジェクトレベルの元に戻す/やり直し」と呼ばれます 削除された変数または変更された変数にはフラグが付けられ、元に戻す/やり直しはそれらのメモリのみを元に戻す/やり直します。そのような場合、「ビジターパターン」の新しいアイデアが使用されます。「オブジェクトレベルの元に戻す/やり直し」と呼ばれます 削除された変数または変更された変数にはフラグが付けられ、元に戻す/やり直しはそれらのメモリのみを元に戻す/やり直します。そのような場合、「ビジターパターン」の新しいアイデアが使用されます。「オブジェクトレベルの元に戻す/やり直し」と呼ばれます

  1. オブジェクトレベルの元に戻す/やり直し:元に戻す/やり直しの通知が呼び出されると、すべてのオブジェクトがストリーミング操作を実装し、ストリーマーはオブジェクトからプログラムされた古いデータ/新しいデータを取得します。乱されていないデータは乱されません。すべてのオブジェクトは引数としてストリーマーを取得し、UNDo / Redo呼び出し内で、オブジェクトのデータをストリーム/ストリーム解除します。

1と2の両方に、1。BeforeUndo()2. AfterUndo()3. BeforeRedo()4. AfterRedo()などのメソッドを含めることができます。これらのメソッドは、(コンテキストコマンドではなく)基本的な元に戻す/やり直しコマンドで公開する必要があります。これにより、すべてのオブジェクトがこれらのメソッドを実装して特定のアクションを実行できるようになります。

良い戦略は、1と2のハイブリッドを作成することです。美しさは、これらのメソッド(1&2)自体がコマンドパターンを使用することです

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.