スプレッドシヌトプロトタむプの䟋を䜿甚したマルチレベルの元に戻す/やり盎し機胜の実装

はじめに



[取り消し]および[やり盎し]ボタンを䜿甚するず、ナヌザヌアクションをキャンセルしお返すこずができ、リストで実行されたすべおのアクションのリストを衚瀺できたす。これらは、ワヌドプロセッサや開発環境、グラフィックおよびCAD゚ディタヌ、システムなどのアプリケヌションの事実䞊の暙準ですサりンドずビデオの線集ず線集。 それらはナヌザヌにずおも銎染みがあるので、埌者は自分の存圚を䞎えられたものずしお認識したす。 しかし、開発者の芳点から芋るず、取り消しの存圚の芁件は、プロゞェクトのアヌキテクチャ党䜓に圱響する芁因の1぀であり、開発プロゞェクトのごく初期の段階で決定されたす。



LibreOfficeおよびGIMPアプリケヌションの元に戻す機胜



オヌプン゜ヌスには、元に戻す/やり盎し機胜を実際に実装する方法に関するかなりの情報がありたす。 E.ガンマらによる叀兞的な本「オブゞェクト指向プログラミングの方法。 「デザむンパタヌン」では、この目的に察する「チヌム」パタヌンの適合性に぀いお簡単に説明しおいたす。むンタヌネットに関する䞻題に関する倚くの䞀般情報がありたすが、十分に完党で十分に開発された実装䟋を芋぀けるこずができたせんでした。 この蚘事では、このギャップを埋めようずし、著者の経隓に基づいお、元に戻す/やり盎しをサポヌトするアプリケヌションアヌキテクチャの詳现な䟋を瀺したす。これは、他のプロゞェクトの基盀ずしお䜿甚できたす。



蚘事のコヌド䟋はJava蚀語で提䟛されおいたすが、Java固有のものはなく、ここで玹介するすべおのアむデアはオブゞェクト指向蚀語に適しおいたす著者自身がDelphiで実装したした。



さたざたなニヌズずアプリケヌションのタむプには、さたざたな「元に戻すモデル」が存圚するこずに泚意しおください。線圢-厳密に逆の順序で操䜜をキャンセルする堎合、および非線圢-実行したアクションの履歎から任意の操䜜をキャンセルする堎合 デヌタモデルぞの同期された倉曎 、぀たり異なるスレッドでのデヌタモデルの内郚状態の同時倉曎が蚱可されおいないシステムでの線圢Undoの実装に぀いお説明したす。 可胜なundoモデルの分類は、たずえばWikipediaの蚘事で提䟛されおいたす 。



圓然、アプリケヌションはビュヌViewからのデヌタモデルModelの分離を実装し、Undo機胜はそのクラスの1぀のundoおよびredoメ゜ッドの圢匏でデヌタモデルレベルで実装されるず仮定したす。



実䟋



説明のための䟋ずしお、この蚘事では、スプレッドシヌトをプロトタむピングするアプリケヌションのデヌタモデルMS Excel / LibreOffice Calcのスタむルに぀いお怜蚎したす。 倀ずサむズを倉曎できるセル、行ず列-亀換するセルで構成されるシヌト簡単にするために1぀のみがあり、これらのアクションはすべおキャンセルされたす。 ゜ヌスコヌドず関連する単䜓テストはhttps://github.com/inponomarev/undoredoで入手でき、Mavenを䜿甚しおコンパむルおよび実行できたす。



この䟋の䞻な゚ンティティは次のずおりです。



  1. worksheet-クラスWorksheet 、
  2. 行ず列- 行ず列クラス AxisElementクラスの子孫、
  3. cells-クラスCell 。


結果の単玔なデヌタモデルは、次の図のクラス図に瀺されおいたす。



説明のためのデヌタモデルクラス



スプレッドシヌトの実装の詳现に぀いおは説明したせんが、゜ヌスコヌド蚭蚈の基本原則に぀いお簡単に説明したす。 スプレッドシヌトシヌトは、境界が画面をはるかに超えるデヌタの2次元配列ずしおナヌザヌに衚瀺されたすが、デヌタモデルに2次元配列を䜿甚するこずは、メモリ消費量たたは䞀般的な操䜜の速床の芳点から正圓化されたせん。 たずえば、MS Excelの以前のバヌゞョンで65536列ず行が蚱可されおいた堎合、32ビットシステムでは65536 2 ぀たり40億セルにメモリを割り圓おるこずは技術的に䞍可胜です。



解決策は、ツリヌずハッシュテヌブルに基づく蟞曞を䜿甚しお、倉曎された倀のみを保存し、蟞曞にない倀をデフォルト倀に眮き換えるこずです。



RowおよびColumnのむンスタンスを栌玍するには、 Worksheetクラスの蟞曞TreeMap <Integer、Row> rowsおよびTreeMap <Integer、Column> columnを䜿甚したす 。 Cellむンスタンスを保存するには、 RowクラスのHashMap <Column、Cell> cells蟞曞を䜿甚したす。 このハッシュテヌブルの倀はCellオブゞェクトぞの参照であり、キヌは列オブゞェクトです。 このデヌタストレヌゞぞのアプロヌチにより、 ワヌクシヌトのコンテンツで実際に必芁なすべおの操䜜に䜿甚される速床ずメモリの最適なバランスを芋぀けるこずができたす。



モデルルヌトクラスずキャンセル可胜なメ゜ッド



この䟋のWorksheetクラスは䞭心です。1他のすべおのビゞネスロゞックオブゞェクトの操䜜は、この特定のクラスのむンスタンスの取埗から始たりたす。2他のクラスのむンスタンスは、 Worksheetオブゞェクトのコンテキストでのみ機胜したす。3 save...メ゜ッドを介しお静的メ゜ッドload...は、ストリヌムに保存し、ストリヌムからシステム党䜓の状態を埩元したす。 このクラスをモデルのルヌトクラスず呌びたす。 原則ずしお、Model-View-Controllerアヌキテクチャでアプリケヌションを開発する堎合、モデルのルヌトクラスが䜕であるかを刀断するのに困難はありたせん。 Undo / Redoの機胜に固有のメ゜ッドが提䟛されるのは圌です。



モデルの状態を倉えるメ゜ッドを特定するのも難しくないはずです。 これらは、呌び出し結果を元に戻す必芁があるメ゜ッドです。 この䟋では、次のずおりです。





もちろん、実際のアプリケヌションでは、さらに倚くの可胜性がありたす。 これらはそれぞれ、察応するチヌムのクラス埌で説明したすず䞀臎する必芁があり、メ゜ッドは特別な方法で曞き換える必芁がありたす。



スタックの取り消しずやり盎し



線圢Undoモデルでは、ドキュメントに察しお実行されたアクションのシヌケンスを保持するような方法で操䜜がキャンセルされたす。 たずえば、最初に列がドキュメントに远加され、次に幅が倉曎された堎合、これらの操䜜のキャンセルは逆の順序でのみ可胜であり、返されるのは盎接です。 したがっお、キャンセルしお埩元する操䜜を栌玍するには、モデルのルヌトクラスの䞀郚であるリンクリストで2぀のスタックを䜿甚するのが自然です。 モデルの状態を倉曎するメ゜ッドが呌び出されるず、Redoスタックがリセットされ、Undoスタックが次の倀で補充されたす。 Undoコマンドを実行するず、Undoスタックから倀が取埗され、Redoスタックにプッシュされたす。 やり盎しコマンドの実行は、もしあれば、再び倀を元に戻すスタックに戻す必芁がありたす図を参照。



スタックの取り消しずやり盎し



これらのスタックの内容は、 Commandクラスの子孫オブゞェクトです。これに぀いおは埌で説明したす。 以䞋は、Undo / Redoの機胜ぞのアクセスを提䟛するビゞネスロゞックのルヌトクラスのパブリックメ゜ッドのリストです。





チヌムパタヌン



モデルの状態を倉曎するメ゜ッドは、さたざたなパラメヌタヌを持ち、通垞はモデルのさたざたなクラスで定矩できたす。 パラメヌタずメ゜ッドのタヌゲットオブゞェクトに関する情報を完党にカプセル化するこずで、「すべおを1぀の櫛の䞋で櫛圢にする」こずができ、蚭蚈パタヌンを「チヌム」にするこずができたす。 このパタヌンの非自明性は、䞀郚の゚ンティティが通垞、オブゞェクト指向コヌドでクラスを蚘述するずいう事実にありたす。 ここでは、クラスは本質ではなく、モデルの状態を倉曎するメ゜ッドによっお実行されるアクションを蚘述し、メ゜ッド自䜓からこの特暩を「取埗」したす。



各コマンドのクラスは、基本抜象クラスCommandから継承したす。 コマンド自䜓には、 execute 、 undo 、およびgetDescriptionの 3぀の抜象メ゜ッドのみがありたす。これらのパラメヌタヌはありたせん重芁です。 これにより、実行たたはキャンセルされた操䜜に぀いお「䜕も知らない」ルヌトクラスのundoおよびredoメ゜ッドを䜿甚しおコマンドを実行およびキャンセルできたす。 getDescriptionメ゜ッドは、アクションのテキスト説明を返す必芁がありたす。この説明は、キャンセルするアクションのリストでナヌザヌが利甚できたす。



コマンドクラスずその子孫



Commandクラスの継承者は、その抜象メ゜ッドの実装に加えお、コマンド起動パラメヌタヌに関する情報ず、既に実行されたコマンドをキャンセルし、実行されたコマンドのテキスト説明を衚瀺するために必芁な情報を含む任意の数の远加フィヌルドを含むこずができたす。 この堎合、 executeメ゜ッドには、通垞、モデルの状態を倉曎するメ゜ッドに含たれるコヌドが含たれおいる必芁がありたすが、メ゜ッドコヌドの代わりにこのコヌドはコマンドクラスのフィヌルドを䜿甚する必芁がありたす。 チヌムは、以前の方法ず同じ方法で、モデルオブゞェクトの内郚状態を操䜜したす。 したがっお、チヌムはモデルオブゞェクトの非衚瀺プラむベヌトフィヌルドにアクセスできる必芁がありたす。 Javaでは、察応するモデルクラスにコマンドの子孫クラスをネストする堎合に䟿利です。 たずえば、アプリケヌションでは、 SetSizeコマンドは AxisElementモデルクラスにネストされ、残りのコマンドはWorksheetにネストされおいたす 。



undoメ゜ッドは、 executeメ゜ッドを呌び出した結果を取り消すこずができるはずです。 これに必芁なすべおの情報は、チヌムクラスのフィヌルドに保存する必芁がありたす。 undoメ゜ッドが呌び出された時点で、ビゞネスロゞックオブゞェクトの状態は、察応するexecuteメ゜ッドの実行盎埌の状態ず垞に同じになるこずを理解すれば、問題は単玔化されたす。 その埌、ナヌザヌが他の操䜜を実行した堎合、珟圚のコマンドの取り消しを行う前に、その埌に呌び出されたすべおのコマンドに察しお取り消しを実行する必芁がありたす。 実際には、この原則を理解するず、 undoメ゜ッドの蚘述が非垞に容易になり、コマンドに保存される情報量が削枛されたす。



セルの倀を蚭定するコマンドの実装を怜蚎しおください。



  final class SetCellValue extends Command { private final int row; private final int col; private String val; public SetCellValue(int row, int col, String newValue) { this.row = row; this.col = col; this.val = newValue; } @Override public String getDescription() { return (" "); } private void changeVal() { String oldValue = getCellValue(row, col); Row r = rows.get(row); Column c = columns.get(col); //....   cell  row  col... cell.setValue(val); //.... val = oldValue; } @Override public void execute() { changeVal(); } @Override public void undo() { changeVal(); } }
      
      





ご芧のずおり、このクラスには、セルのアドレスずその倀を保存するための倉数がありたす。 たた、メモリを節玄するために、倀を保存する倉数は1぀だけです。executeメ゜ッドがただ実行されおいない堎合は新しい倉数、 executeメ゜ッドが既に実行されおいる堎合は叀い倉数です。 ぀たり、 executeおよびundoメ゜ッドが順番に実行されるずいう事実がここで䜿甚されたす。 getDescriptionメ゜ッドはクラス倉数を䜿甚しお、コマンドのより詳现な説明を提䟛できたす。



メ゜ッドテンプレヌトのキャンセル



コマンドはキャンセル可胜なメ゜ッドでどのように䜿甚されたすか 通垞、そのようなメ゜ッドがパラメヌタヌを考慮しおモデルでいく぀かのアクションを実行する堎合、それらすべおを元に戻すシステムでは、厳密に次の3぀の操䜜を実行する必芁がありたす。



  1. 察応するコマンドのむンスタンスを䜜成したす コマンドの子孫クラス。
  2. メ゜ッドパラメヌタず堎合によっおは远加情報でコマンドフィヌルドを初期化したす。
  3. ルヌトオブゞェクトのexecuteCommand cmdメ゜ッドを実行し、新しく䜜成および初期化されたコマンドをパラメヌタヌずしお枡したす。


この䟋では、 setCellValueメ゜ッドの実装は次のようになりたす。



 public void setCellValue(int row, int col, String value) { Command cmd = new SetCellValue(row, col, value); execute(cmd); }
      
      





他のすべおのキャンセル可胜なメ゜ッドは䌌おいたす。



ルヌトクラスのexecuteCommand cmdメ゜ッドはコマンドアクションを実行し、REDOスタックをダンプし、コマンドを元に戻すスタックにプッシュしたす。



  undoStack.push(cmd); redoStack.clear(); cmd.execute();
      
      





この瞬間から、チヌムはキャンセル/繰り返されるアクションのチェヌンの䞀郚になりたす。 䞊蚘のように、ルヌトクラスでundoメ゜ッドを呌び出すず、Undoスタックの最䞊郚にあるコマンドのundoメ゜ッドが呌び出され、Redoスタックに転送されたす。 次に、ルヌトクラスのredoメ゜ッドを呌び出すず、Redoスタックの最䞊郚でコマンドのexecuteメ゜ッドが実行され、Undoスタックにプッシュされたす。



コマンドクラスの再利甚


そのため、通垞は1぀のメ゜ッドを蚘述する必芁があるタスクの堎合、元に戻す機胜を備えたシステムでは、クラス党䜓を䜜成する必芁がありたす。これにより、コヌドの量ずそのサポヌトの耇雑さに぀いお正圓な恐れが生じたす。実際のプロゞェクトでは、キャンセルされたメ゜ッドの数は数十になりたす。



実際、コマンドクラスは、その汎甚性ず耇数のキャンセルされたメ゜ッドに1぀のコマンドクラスを䜿甚しおいるため、倧幅に小さくなりたす。 たずえば、実際には、 SetCellValueクラスのメむンコヌドは、倉数の1぀の倀を倉曎するすべおのメ゜ッドに䜿甚できたす。 フィヌルドを远加するか、クラスをパラメヌタヌ化するこずで、コマンドクラスをより汎甚的にするこずができたす。



たずえば、テヌブルの行ず列の䞡方を削陀するために䜿甚されるナニバヌサル削陀コマンドを考えたす。



  final class Delete<T extends AxisElement> extends Command { private final int num; private final T deleted; private final TreeMap<Integer, T> map; Delete(TreeMap<Integer, T> map, int num) { this.num = num; this.map = map; deleted = map.get(num); } @Override public String getDescription() { return String.format(" %s %d", map == columns ? "" : "", num); } @Override public void execute() { internalDelete(map, num); } @Override public void undo() { internalInsert(map, num); map.put(num, deleted); } } private static <T extends AxisElement> void internalDelete(TreeMap<Integer, T> map, int num) { //... //      num //      > num     //... } private static <T extends AxisElement> void internalInsert(TreeMap<Integer, T> map, int num) { //... //     >= num     //... } }
      
      





deleteColumnおよびdeleteRowメ゜ッドでの䜿甚は次のずおりです。



  public void deleteColumn(int colNum) { Command cmd = new Delete<Column>(columns, colNum); execute(cmd); } public void deleteRow(int rowNum) { Command cmd = new Delete<Row>(rows, rowNum); execute(cmd); }
      
      





マクロ



状態を倉曎するメ゜ッド呌び出しが、Undoスタックに栌玍するには単䜍が小さすぎるこずが刀明する堎合がありたす。 2次元リストクリップボヌドなどからドキュメントに倀を貌り付けるには、プロシヌゞャinsertValuesint top、int left、String [] [] valueを怜蚎しおください。 ルヌプ内のこのプロシヌゞャは、バッファのセルの倀でドキュメントセルを1぀ず぀曎新したす。 したがっお、4×4テヌブルの䞀郚を挿入するず、元に戻すメカニズムの芳点から、ドキュメントセルに16の倉曎が加えられたす。 これは、ナヌザヌが挿入の結果をキャンセルする堎合、[元に戻す]ボタンを16回抌す必芁がありたすが、衚では、16個のセルが以前の倀を次々ず埩元するこずを意味したす。



もちろん、これは間違っおいたす。このような操䜜の結果は、キャンセルしお党䜓ずしお埩元し、キャンセルされた操䜜のリストに1行で衚瀺する必芁がありたす。 これを可胜にするために、マクロが䜿甚されたす。



マクロを実装するずいう考え方は単玔です。これは、図に瀺すように、 コマンドクラスの特別な子孫であり、その䞭に他のコマンドのチェヌンが含たれおいたす。



マクロスタックを元に戻す



MacroCommandクラスのexecuteメ゜ッドは、独自のコマンドリストを実行し、それらのexecuteメ゜ッドを実行したす。 同じマクロのundoメ゜ッドを呌び出すず、同じコマンドのリストを逆の順序で実行し、それらのundoメ゜ッドを呌び出したす。



モデル/ビュヌ/コントロヌラヌアヌキテクチャで構築されたアプリケヌションのクリップボヌドペヌストメ゜ッドに類䌌したマクロメ゜ッドは、原則ずしおモデルの䞀郚ではありたせんが、コントロヌラヌレベルで実装されたす。 倚くの堎合、これらは、ある皮の日垞的な䜜業の自動化のみを衚し、その必芁性は、ナヌザヌむンタヌフェむスの皮類に応じお、存圚する堎合ず存圚しない堎合がありたす。 さらに、倚くの堎合、耇数のナヌザヌアクションを1぀にグルヌプ化する必芁がありたす。たずえば、テキスト゚ディタヌは、個々の文字を入力するための゚ントリで元に戻すスタックを詰たらせるのではなく、単語ず文のナヌザヌ入力を1぀のマクロアクションにグルヌプ化したす。



したがっお、マクロサポヌトは、アプリケヌションに䟝存しない方法で、抜象レベルで実装できたす。 これは、パブリックメ゜ッドbeginMacro文字列の説明ずendMacroをモデルルヌトクラスに远加するこずで実行されたす。 メ゜ッドは、マクロアクションの前埌に呌び出されたす。 beginMacro...を、キャンセルされた操䜜のリストでナヌザヌが䜿甚できる文字列パラメヌタヌで呌び出すこずにより、 MacroCommand型のオブゞェクトを生成し、Undoスタックを内郚マクロスタックで䞀時的に眮き換えたす。 したがっお、beginMacroを呌び出した埌、ルヌトクラスのexecute...メ゜ッドにコマンドを転送するず、Undoスタックに盎接曞き蟌たれるのではなく、珟圚のマクロの内郚スタックに曞き蟌たれたす順番に、すでにUndoスタックに曞き蟌たれたす  endMacro呌び出しは、すべおをその堎所に戻したす。 マクロを盞互にマルチレベルでネストするこずもできたす。



未保存の倉曎を远跡する



元に戻す機胜を䜿甚するず、ドキュメントに保存されおいない倉曎を远跡する信頌できる方法が提䟛されたす。 これは、アプリケヌションの「保存」ボタンの正しい動䜜を実装するために必芁です。



  1. 「保存」ボタンは、未保存の倉曎が存圚する堎合にのみアクティブにする必芁がありたすそうでない堎合は保存する必芁はありたせん。ドキュメントは倉曎されおいたせん。
  2. ドキュメントを閉じるずきに、未保存の倉曎が存圚する堎合にのみ倉曎を保存するかどうかをナヌザヌに尋ねるこずは理にかなっおいたす。


この䟋では、未保存の倉曎の存圚がisModifiedメ゜ッドによっお返されたす。 次のように実装されたす。save ...メ゜ッドが呌び出されるたびに、Undoスタックの珟圚のトップがlastSavedPoint倉数に保存されたす。 isModifiedメ゜ッドが呌び出されるず、 Undoスタックの珟圚のトップがlastSavedPoint倀ず比范されたす。等しい堎合、保存されおいない倉曎はありたせん。 元に戻すメカニズムを無効にするず、 isModifiedメ゜ッドは垞にtrueを返したす 。



おわりに



私たちの意芋では、他のプロゞェクトのテンプレヌトずしお十分に普遍的な䟋を䜿甚しお、線圢マルチレベルの元に戻す/やり盎しを実装するための基本原則を怜蚎したした。



元に戻すずやり盎しの機胜が、アプリケヌションのアヌキテクチャず開発者のプロ意識に察しお非垞に深刻な芁求をするこずは驚くこずではありたせん。 しかし、Model / View / Controllerアヌキテクチャの厳密な遵守や適切に蚭蚈されたモデルシステムのモデルの状態を倉曎するメ゜ッドを元に戻すこずは「費甚がかかる」など、面倒な䜜業にもかかわらず、䜜成䞭のプログラムの高品質ず信頌性が埗られたす。最終的にはナヌザヌの満足に぀ながりたす。



* * *



この蚘事で説明されおいる䟋の完党な゜ヌスコヌドず察応する単䜓テストはhttps://github.com/inponomarev/undoredoで入手でき、Mavenを䜿甚しおコンパむルおよび実行できたす。



All Articles