使用 Memento 模式(和命令)存储复杂对象的状态
我正在开发一个 Java 小型 UML 编辑器项目,该项目是我几个月前开始的。 几周后,我得到了 UML 类图编辑器的工作副本。
但现在,我正在完全重新设计它以支持其他类型的图,例如序列、状态、类等。这是通过实现图构建框架来完成的(我受到了 Cay Horstmann 在该主题上的工作的极大启发)紫罗兰 UML 编辑器)。
重新设计进行得很顺利,直到我的一位朋友告诉我,我忘记向项目添加“执行/撤消”功能,在我看来,这至关重要。
记得面向对象的设计课程,我立即想到了 Memento 和 Command 模式。
事情是这样的。 我有一个抽象类 AbstractDiagram,它包含两个 ArrayList:一个用于存储节点(在我的项目中称为 Elements),另一个用于存储边(在我的项目中称为 Links)。 该图可能会保留一堆可以撤消/重做的命令。 相当标准。
我怎样才能有效地执行这些命令? 举例来说,我想移动一个节点(该节点将是一个名为 INode 的接口类型,并且将有从它派生的具体节点(ClassNode、InterfaceNode、NoteNode 等))。
位置信息作为节点中的属性保存,因此通过修改节点本身的该属性,可以更改状态。 当刷新显示时,节点将发生移动。 这是模式的纪念品部分(我认为),不同之处在于对象是状态本身。
此外,如果我保留原始节点的克隆(在移动之前),我可以恢复到其旧版本。 同样的技术适用于节点中包含的信息(类或接口名称、注释节点的文本、属性名称等)。
问题是,如何在撤消/重做操作时用其克隆替换图中的节点? 如果我克隆图表引用的原始对象(位于节点列表中),则克隆不是图表中的引用,唯一指向的是命令本身! 我是否应该在图中包含根据 ID 查找节点的机制(例如),以便我可以在图中用其克隆替换节点(反之亦然)? 这取决于 Memento 和 Command 模式吗? 链接呢? 它们也应该是可移动的,但我不想创建一个仅用于链接的命令(以及一个仅用于节点的命令),并且我应该能够根据命令的对象类型修改正确的列表(节点或链接)指的是.
你将如何进行? 简而言之,我在以命令/备忘录模式表示对象的状态时遇到困难,以便可以有效地恢复它并在图表列表中恢复原始对象,并且取决于对象类型(节点或链接)。
多谢!
纪尧姆.
PS:如果我不清楚,请告诉我,我会澄清我的信息(一如既往!)。
编辑
这是我的实际解决方案,我在发布此问题之前开始实施。
首先,我定义了一个 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,我不必将图传递到实际的节点或链接,以便它可以从图中删除自己(我认为这是一个坏主意)。
摘要图解图; 可添加对象; 添加删除类型类型;
@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 合并。 这个班级目前是空的。 我可能会使用一种机制来确定修改的对象是节点还是边(通过 instanceof 或 ID 中的特殊表示)。
这是一个好的解决方案吗?
I'm working on a small UML editor project, in Java, that I started a couple of months ago. After a few weeks, I got a working copy for a UML class diagram editor.
But now, I'm redesigning it completely to support other types of diagrams, such a sequence, state, class, etc. This is done by implementing a graph construction framework (I'm greatly inspired by Cay Horstmann work on the subject with the Violet UML editor).
Redesign was going smoothly until one of my friends told me that I forgot to add a Do/Undo functionnality to the project, which, in my opinion, is vital.
Remembering object oriented design courses, I immediately thought of Memento and Command pattern.
Here's the deal. I have a abstract class, AbstractDiagram, that contains two ArrayLists : one for storing nodes (called Elements in my project) and the other for storing Edges (called Links in my projects). The diagram will probably keep a stack of commands that can be Undoed/Redoed. Pretty standard.
How can I execute these commands in a efficient way? Say, for example, that I want to move a node (the node will be an interface type named INode, and there will be concrete nodes derived from it (ClassNode, InterfaceNode, NoteNode, etc.)).
The position information is held as an attribute in the node, so by modying that attribute in the node itself, the state is changed. When the display will be refreshed, the node will have moved. This is the Memento part of the pattern (I think), with the difference that the object is the state itself.
Moreover, if I keep a clone of the original node (before it moved), I can get back to its old version. The same technique applies for the information contained in the node (the class or interface name, the text for a note node, the attributes name, and so on).
The thing is, how do I replace, in the diagram, the node with its clone upon undo/redo operation? If I clone the original object that is referenced by the diagram (being in the node list), the clone isn't reference in the diagram, and the only thing that points to is the Command itself! Shoud I include mechanisms in the diagram for finding a node according to an ID (for example) so I can replace, in the diagram, the node by its clone (and vice-versa) ? Is it up to the Memento and Command patterns to do that ? What about links? They should be movable too but I don't want to create a command just for links (and one just for nodes), and I should be able to modify the right list (nodes or links) according to the type of the object the command is referring to.
How would you proceed? In short, I am having trouble representing the state of an object in a command/memento pattern so that it can be efficiently recovered and the original object restored in the diagram list, and depending on the object type (node or link).
Thanks a lot!
Guillaume.
P.S.: if I'm not clear, tell me and I will clarify my message (as always!).
Edit
Here's my actual solution, that I started implementing before posting this question.
First, I have an AbstractCommand class defined as follow :
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();
}
Then, each type of command is implemented using a concrete derivation of AbstractCommand.
So I have a command to move an object :
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;
}
}
I also have a MoveRemoveCommand (to... move or remove an object/node). If I use the ID of instanceof method, I don't have to pass the diagram to the actual node or link so that it can remove itself from the diagram (which is a bad idea I think).
AbstractDiagram diagram;
Addable obj;
AddRemoveType type;
@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;
}
}
}
Finally, I have a ModificationCommand which is used to modify the info of a node or link (class name, etc.). This may be merged in the future with the MoveCommand. This class is empty for now. I will probably do the ID thing with a mechanism to determine if the modified object is a node or an edge (via instanceof or a special denotion in the ID).
Is this is a good solution?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我认为你只需要把你的问题分解成更小的问题。
第一个问题:
问:如何使用备忘录/命令模式表示应用程序中的步骤?
首先,我不知道你的应用程序到底是如何工作的,但希望你能明白我的意思。 假设我想在图表上放置一个具有以下属性的 ClassNode,
它将被包装为命令对象。 假设它进入了一个DiagramController。 然后,图控制器的职责可以是记录该命令(我的赌注是推入堆栈)并将命令传递给例如DiagramBuilder。 图构建器实际上负责更新显示。
类似的事情应该可以做到,当然还有很多细节需要解决。 顺便说一句,节点的属性越多,Undraw 就越详细。
使用 id 将堆栈中的命令链接到绘制的元素可能是一个好主意。 可能看起来像这样:
此时,您不一定必须拥有该对象,因为您拥有 ID,但如果您决定还实现重做功能,它将很有帮助。 为节点生成 id 的一个好方法是为它们实现哈希码方法,但不能保证您不会以导致哈希码相同的方式重复节点。
问题的下一部分是在您的DiagramBuilder 中,因为您试图找出到底如何处理这些命令。 为此,我所能说的就是确保您可以为您可以添加的每种类型的组件创建相反的操作。 要处理取消链接,您可以查看边缘连接属性(我认为是代码中的链接)并通知每个边缘连接它们将与特定节点断开连接。 我认为在断开连接时他们可以适当地重新绘制自己。
总而言之,我建议不要保留对堆栈中节点的引用,而只保留一种表示给定节点当时状态的令牌。 这将允许您在多个位置表示撤消堆栈中的同一节点,而无需引用同一对象。
如果你有Q的话就发帖吧。 这是一个复杂的问题。
I think you just need to decompose your problem into smaller ones.
First problem:
Q: How to represent the steps in your app with the memento/command pattern?
First off, I have no idea exactly how your app works but hopefully you will see where I am going with this. Say I want to place a ClassNode on the diagram that with the following properties
That would be wrapped up as a command object. Say that goes to a DiagramController. Then the diagram controller's responsibility can be to record that command (push onto a stack would be my bet) and pass the command to a DiagramBuilder for example. The DiagramBuilder would actually be responsible for updating the display.
Some thing like that should do it and of course there will be plenty of details to sort out. By the way, the more properties your nodes have the more detailed Undraw is going to have to be.
Using an id to link the command in your stack to the element drawn might be a good idea. That might look like this:
At this point you don't absolutely have to have the object since you have the ID but it will be helpful should you decide to also implement redo functionality. A good way to generate the id for your nodes would be to implement a hashcode method for them except for the fact that you wouldn't be guaranteed not to duplicate your nodes in such a way that would cause the hash code to be identical.
The next part of the problem is within your DiagramBuilder because you're trying to figure out how the heck to deal with these commands. For that all I can say is to really just ensure you can create an inverse action for each type of component you can add. To handle the delinking you can look at the edge-connection property (links in your code I think) and notify each of the edge-connections that they are to disconnect from the specific node. I would assume that on disconnection they could redraw themselves appropriately.
To kinda summarize, I would recommend not keeping a reference to your nodes in the stack but instead just a kind of token that represents a given node's state at that point. This will allow you to represent the same node in your undo stack at multiple places without it referring to the same object.
Post if you've got Q's. This is a complex issue.
以我的拙见,您的思考方式比实际情况更复杂。 为了恢复到以前的状态,根本不需要克隆整个节点。 相反,每个**Command 类都会有 -
由于命令类引用了节点,因此我们不需要 ID 机制来引用图中的对象。
在您问题的示例中,我们希望将节点移动到新位置。 为此,我们有一个 NodePositionChangeCommand 类。
我在 GoF 书中(在备忘录模式讨论中)读到,链接的移动与节点位置的变化是由某种约束求解器处理的。
In my humble opinion, you're thinking it in a more complicated way than it really is. In order to revert to previous state, clone of whole node is not required at all. Rather each **Command class will have -
Since command classes have reference to the node, we do not need ID mechanism to refer to objects in the diagram.
In the example from your question, we want to move a node to a new position. For that, we have a NodePositionChangeCommand class.
I read in GoF book (in memento pattern discussion) that move of link with change in position of nodes are handled by some kind of constraint solver.