Memento パターン (およびコマンド) を使用した複雑なオブジェクトの状態の保存
-
03-07-2019 - |
質問
私は数か月前に始めた、Java による小さな UML エディター プロジェクトに取り組んでいます。数週間後、UML クラス図エディターの作業用コピーを入手しました。
しかし現在、シーケンス、状態、クラスなどの他のタイプの図をサポートするために完全に再設計しています。これは、グラフ構築フレームワークを実装することによって行われます (私は、Cay Horstmann が Violet UML エディターを使用してこのテーマに取り組んだことに大きな影響を受けています)。
再設計は順調に進んでいたのですが、友人の一人が、プロジェクトに「実行/元に戻す」機能を追加するのを忘れたと教えてくれました。私の意見では、これは非常に重要なことです。
オブジェクト指向設計のコースを思い出して、すぐに Memento と Command パターンを思い出しました。
これが契約です。2 つの ArrayList を含む抽象クラス AbstractDiagram があります。1 つはノード (プロジェクトでは要素と呼ばれます) を格納するためのもので、もう 1 つはエッジ (プロジェクトではリンクと呼ばれます) を格納するためのものです。この図には、おそらく元に戻す/やり直しできるコマンドのスタックが保持されます。かなり標準的です。
これらのコマンドを効率的に実行するにはどうすればよいですか?たとえば、ノードを移動したいとします (ノードは INode という名前のインターフェイス タイプになり、そこから派生した具体的なノード (ClassNode、InterfaceNode、NoteNode など) が存在します)。
位置情報はノード内で属性として保持されているため、ノード自体のその属性を変更することで状態を変更します。表示が更新されると、ノードが移動したことになります。これはパターンの Memento 部分です (私はそう思います) が、オブジェクトが状態そのものであるという点が異なります。
さらに、元のノード (移動前) のクローンを保持しておけば、古いバージョンに戻すことができます。同じ手法が、ノードに含まれる情報 (クラス名またはインターフェイス名、ノート ノードのテキスト、属性名など) にも適用されます。
問題は、元に戻す/やり直し操作の際に、図内のノードをそのクローンに置き換えるにはどうすればよいでしょうか?図で参照されている元のオブジェクト (ノード リスト内にある) のクローンを作成すると、そのクローンは図では参照されず、指すのはコマンド自体だけになります。図内でノードをそのクローンで置き換えることができるように(またはその逆も同様)、ID に従ってノードを検索するためのメカニズムを図に含めるべきでしょうか?それを行うのは Memento パターンと Command パターン次第ですか?リンクについてはどうですか?それらも移動可能である必要がありますが、リンク専用のコマンド(およびノード専用のコマンドも作成したくありません)、コマンドのオブジェクトのタイプに応じて適切なリスト(ノードまたはリンク)を変更できる必要がありますを指しています。
どのように進めますか?つまり、オブジェクトの種類 (ノードまたはリンク) に応じて、オブジェクトの状態をコマンド/メメント パターンで表現して効率的に回復し、元のオブジェクトをダイアグラム リストに復元できるようにするのに苦労しています。
どうもありがとう!
ギョーム。
追記:明確でない場合は、言ってください。メッセージを明確にさせていただきます (いつものように!)。
編集
これが私の実際の解決策であり、この質問を投稿する前に実装を開始しました。
まず、次のように AbstractCommand クラスを定義します。
public abstract class AbstractCommand {
public boolean blnComplete;
public void setComplete(boolean complete) {
this.blnComplete = complete;
}
public boolean isComplete() {
return this.blnComplete;
}
public abstract void execute();
public abstract void unexecute();
}
次に、AbstractCommand の具体的な派生を使用して、各タイプのコマンドが実装されます。
したがって、オブジェクトを移動するコマンドがあります。
public class MoveCommand extends AbstractCommand {
Moveable movingObject;
Point2D startPos;
Point2D endPos;
public MoveCommand(Point2D start) {
this.startPos = start;
}
public void execute() {
if(this.movingObject != null && this.endPos != null)
this.movingObject.moveTo(this.endPos);
}
public void unexecute() {
if(this.movingObject != null && this.startPos != null)
this.movingObject.moveTo(this.startPos);
}
public void setStart(Point2D start) {
this.startPos = start;
}
public void setEnd(Point2D end) {
this.endPos = end;
}
}
また、MoveRemoveCommand (目的) もあります。オブジェクト/ノードを移動または削除します)。Instanceof メソッドの ID を使用すると、ダイアグラムを実際のノードまたはリンクに渡してダイアグラムから自身を削除する必要がなくなります (これは悪い考えだと思います)。
AbstractDiagram ダイアグラム;追加可能なオブジェクト。AddRemoveType タイプ。
@SuppressWarnings("unused")
private AddRemoveCommand() {}
public AddRemoveCommand(AbstractDiagram diagram, Addable obj, AddRemoveType type) {
this.diagram = diagram;
this.obj = obj;
this.type = type;
}
public void execute() {
if(obj != null && diagram != null) {
switch(type) {
case ADD:
this.obj.addToDiagram(diagram);
break;
case REMOVE:
this.obj.removeFromDiagram(diagram);
break;
}
}
}
public void unexecute() {
if(obj != null && diagram != null) {
switch(type) {
case ADD:
this.obj.removeFromDiagram(diagram);
break;
case REMOVE:
this.obj.addToDiagram(diagram);
break;
}
}
}
最後に、ノードまたはリンクの情報 (クラス名など) を変更するために使用される ModificationCommand があります。これは将来 MoveCommand と統合される可能性があります。このクラスは今のところ空です。おそらく、変更されたオブジェクトがノードであるかエッジであるかを判断するメカニズムを使用して ID 処理を実行するでしょう (instanceof または ID 内の特別な表記を介して)。
これは良い解決策でしょうか?
解決
問題をより小さな問題に分解する必要があると思います。
最初の問題:質問:アプリ内のステップをメモ/コマンド パターンで表すにはどうすればよいですか?まず、あなたのアプリがどのように機能するのか正確にはわかりませんが、私がこれでどこに行くのかを理解していただければ幸いです。次のプロパティを持つ ClassNode を図上に配置したいとします。
{ width:100, height:50, position:(10,25), content:"Am I certain?", edge-connections:null}
これはコマンド オブジェクトとしてラップされます。それが DiagramController に行くとします。次に、ダイアグラム コントローラーの役割は、そのコマンドを記録し (スタックにプッシュするのが確実です)、コマンドをたとえば DiagramBuilder に渡すことになります。実際には、DiagramBuilder が表示の更新を担当します。
DiagramController
{
public DiagramController(diagramBuilder:DiagramBuilder)
{
this._diagramBuilder = diagramBuilder;
this._commandStack = new Stack();
}
public void Add(node:ConditionalNode)
{
this._commandStack.push(node);
this._diagramBuilder.Draw(node);
}
public void Undo()
{
var node = this._commandStack.pop();
this._diagramBuilderUndraw(node);
}
}
そのようなものはそれを行うべきであり、もちろん、整理すべき詳細はたくさんあるでしょう。ちなみに、ノードのプロパティが多いほど、Undraw はより詳細に行う必要があります。
ID を使用して、スタック内のコマンドを描画された要素にリンクすることは良いアイデアかもしれません。それは次のようになります。
DiagramController
{
public DiagramController(diagramBuilder:DiagramBuilder)
{
this._diagramBuilder = diagramBuilder;
this._commandStack = new Stack();
}
public void Add(node:ConditionalNode)
{
string graphicalRefId = this._diagramBuilder.Draw(node);
var nodePair = new KeyValuePair<string, ConditionalNode> (graphicalRefId, node);
this._commandStack.push(nodePair);
}
public void Undo()
{
var nodePair = this._commandStack.pop();
this._diagramBuilderUndraw(nodePair.Key);
}
}
現時点では、絶対にそうする必要はありません 持たなければならない ID があるのでオブジェクトですが、やり直し機能も実装することに決めた場合に役立ちます。ノードの ID を生成する良い方法は、ハッシュ コードが同一になるような方法でノードが複製されないという保証がないことを除いて、ハッシュコード メソッドを実装することです。
問題の次の部分は DiagramBuilder 内にあり、これらのコマンドを一体どのように処理するかを理解しようとしているからです。そのために私が言えることは、追加できるコンポーネントの種類ごとに逆アクションを作成できることを実際に確認することだけです。リンク解除を処理するには、エッジ接続プロパティ (コード内のリンクだと思います) を確認し、特定のノードから切断することを各エッジ接続に通知します。切断すると、適切に再描画できると思います。
簡単にまとめると、スタック内のノードへの参照を保持するのではなく、その時点での特定のノードの状態を表す一種のトークンのみを保持することをお勧めします。これにより、同じオブジェクトを参照せずに、元に戻すスタック内の複数の場所で同じノードを表すことができます。
Qがある場合は投稿してください。これは複雑な問題です。
他のヒント
私の謙虚な意見では、あなたは実際よりも複雑に考えていると思います。以前の状態に戻すために、ノード全体のクローンを作成する必要はまったくありません。むしろそれぞれ **コマンドクラスには -
- 動作しているノードへの参照、
- memento オブジェクト (ノードが戻るのに十分な状態変数を持つ)
- execute() メソッド
- undo() メソッド。
コマンド クラスはノードへの参照を持っているため、図内のオブジェクトを参照するための ID メカニズムは必要ありません。
あなたの質問の例では、ノードを新しい位置に移動したいと考えています。そのために、NodePositionChangeCommand クラスがあります。
public class NodePositionChangeCommand {
// This command will act upon this node
private Node node;
// Old state is stored here
private NodePositionMemento previousNodePosition;
NodePositionChangeCommand(Node node) {
this.node = node;
}
public void execute(NodePositionMemento newPosition) {
// Save current state in memento object previousNodePosition
// Act upon this.node
}
public void undo() {
// Update this.node object with values from this.previousNodePosition
}
}
リンクについてはどうですか?これらも移動可能である必要がありますが、リンク専用のコマンド (ノード専用のコマンドも) を作成したくありません。
GoF の本 (パターンに関する議論の思い出) で、ノードの位置の変更に伴うリンクの移動は、ある種の制約ソルバーによって処理されると読みました。