我正在研究Java中的一个小型UML编辑器项目,这是我几个月前开始的。几周后,我得到了一个UML类图编辑器的工作副本。
但是现在,我正在重新设计它以支持其他类型的图表,例如序列,状态,类等。这是通过实现图形构建框架来完成的(我很大程度上受到了Cay Horstmann工作的启发。主题为Violet UML编辑器。)
重新设计进展顺利,直到我的一位朋友告诉我,我忘了在项目中添加Do / Undo功能,在我看来,这是至关重要的。
记住面向对象的设计课程,我立即想到了Memento和Command模式。
这是交易。我有一个抽象类AbstractDiagram,它包含两个ArrayLists:一个用于存储节点(在我的项目中称为Elements),另一个用于存储Edges(在我的项目中称为Links)。该图可能会保留一堆可以撤消/重做的命令。很标准。
如何以有效的方式执行这些命令?比如说,我想移动一个节点(该节点将是一个名为INode的接口类型,并且将从中派生具体节点(ClassNode,InterfaceNode,NoteNode等))。
位置信息作为节点中的属性保存,因此通过在节点本身中修改该属性,状态会发生变化。当刷新显示时,节点将移动。这是模式中的Memento部分(我认为),区别在于对象是状态本身。
此外,如果我保留原始节点的克隆(在它移动之前),我可以回到它的旧版本。相同的技术适用于节点中包含的信息(类或接口名称,注释节点的文本,属性名称等)。
问题是,如何在图中用undo / redo操作替换节点及其克隆?如果我克隆图中引用的原始对象(在节点列表中),则克隆不是图中的引用,唯一指向的就是Command本身! Shoud我在图中包含了根据ID查找节点的机制(例如)所以我可以在图中用它的克隆替换节点(反之亦然)?这是由Memento和Command模式来完成的吗?链接怎么样?它们也应该是可移动的,但我不想为链接创建一个命令(并且只为节点创建一个),我应该能够根据命令对象的类型修改正确的列表(节点或链接)指的是。
你会怎么做?简而言之,我无法在命令/纪念模式中表示对象的状态,以便可以有效地恢复它并且原始对象在图表列表中恢复,并且取决于对象类型(节点或链接)。
非常感谢!
纪尧姆。
P.S。:如果我不清楚,请告诉我,我会澄清我的信息(一如既往!)。
修改
这是我的实际解决方案,我在发布此问题之前就已开始实施。
首先,我有一个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中的特殊标记)。
这是一个很好的解决方案吗?
答案 0 :(得分:4)
我认为您只需将问题分解为较小的问题。
第一个问题: 问:如何用memento /命令模式表示应用程序中的步骤? 首先,我不知道你的应用程序究竟是如何运作的,但希望你会看到我的目标。假设我想在具有以下属性的图表上放置一个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的一种好方法是为它们实现一个hashcode方法,除非你不能保证不会以一种会导致哈希码相同的方式复制你的节点。 / p>
问题的下一部分是在你的DiagramBuilder中,因为你正在试图弄清楚如何处理这些命令。尽管如此,我只能确保您可以为可以添加的每种类型的组件创建反向操作。要处理脱钩,您可以查看edge-connection属性(我认为代码中的链接),并通知每个边连接它们要从特定节点断开连接。我认为断线时他们可以适当地重绘。
总而言之,我建议不要在堆栈中保留对节点的引用,而应该只是一种表示该点上给定节点状态的令牌。这将允许您在多个位置表示撤消堆栈中的同一节点,而不会引用同一个对象。
如果你有Q,请发帖。这是一个复杂的问题。
答案 1 :(得分:1)
以我的拙见,你会以一种比实际更复杂的方式来思考它。为了恢复到先前的状态,根本不需要克隆整个节点。而是每个 * * Command类都有 -
由于命令类引用了节点,因此我们不需要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书中(在纪念模式讨论中)读到了节点位置变化的链接移动由某种约束求解器处理。