2008-10-17 25 views
4

我正在开发几个月前创建的Java中的一个小型UML编辑器项目。几周后,我得到了一个UML类图编辑器的工作副本。但现在,我正在重新设计它以支持其他类型的图表,例如序列,状态,类等等。这是通过实现图形构建框架完成的(我受到了Cay Horstmann的大力启发使用Violet UML编辑器的主题)。用纪念图案(和命令)存储复杂对象的状态

重新设计工作进展顺利,直到我的一位朋友告诉我忘记为项目添加撤销/撤消功能,而在我看来,这一功能至关重要。

记住面向对象的设计课程,我立即想到了Memento和Command模式。

这是交易。我有一个抽象类AbstractDiagram,它包含两个ArrayList:一个用于存储节点(在我的项目中称为元素),另一个用于存储边(在我的项目中称为链接)。该图可能会保留一堆可以撤销/重做的命令。相当标准。

如何以有效的方式执行这些命令?举例来说,我想移动一个节点(该节点将是一个名为INode的接口类型,并且会从它派生出具体的节点(ClassNode,InterfaceNode,NoteNode等))。

位置信息作为一个属性保存在节点中,所以通过在节点本身中实现该属性来改变状态。当显示刷新时,节点将移动。这是该模式的Memento部分(我认为),区别在于对象是状态本身。此外,如果我保留原始节点的克隆(移动之前),我可以回到它的旧版本。同样的技术适用于节点中包含的信息(类或接口名称,备注节点的文本,属性名称等)。

问题是,在撤销/重做操作时,如何在图中用克隆代替节点?如果我克隆图中引用的原始对象(位于节点列表中),则克隆不在图中引用,唯一指向的是命令本身!首先,我在图表中包含根据ID查找节点的机制(例如),以便我可以在图中用克隆(反之亦然)替换节点?这是否由纪念品和命令模式来做到这一点?链接怎么样?它们也应该是可移动的,但我不想仅为链接创建一个命令(并且仅针对节点),并且我应该能够根据命令对象的类型修改正确的列表(节点或链接)指的是。

你将如何进行?简而言之,我无法在命令/备忘录模式中表示对象的状态,以便可以高效地恢复对象,并在图表列表中恢复原始对象,并根据对象类型(节点或链接)进行恢复。

非常感谢!

Guillaume。

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 diagram; Addable obj; 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合并。现在这个类是空的。我可能会使用一种机制来确定修改后的对象是节点还是边缘(通过instanceof或ID中的特殊含义)。

这是一个很好的解决方案吗?

回答

4

我认为你只需要将你的问题分解成更小的问题。

第一个问题: 问:如何使用备忘录/命令模式表示应用程序中的步骤? 首先,我不知道你的应用程序的工作原理,但希望你能看到我的目标。假设我想在具有以下属性的图表上放置ClassNode

{ width:100, height:50, position:(10,25), content:"Am I certain?", edge-connections:null} 

这将被封装为一个命令对象。说到一个图表控制器。然后图表控制者的责任可以是记录该命令(推入堆栈将是我的赌注),并将命令传递给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方法,除非你不能保证不会以使hash编码相同的方式复制节点。

问题的下一部分是在您的DiagramBuilder中,因为您正试图弄清楚如何处理这些命令。为此,我只能说确实可以确保您可以为每种可添加组件的类型创建反向操作。要处理断开连接,可以查看边连接属性(我认为代码中的链接),并通知每个边连接它们将与特定节点断开连接。我会假设在断开连接时他们可以适当地重新绘制自己。总之,我建议不要在堆栈中保留对节点的引用,而应该只是一种表示给定节点状态的标记。这将允许您在多个地方展示撤销堆栈中的同一个节点,而不会引用同一个对象。

如果您有问题,请发帖。这是一个复杂的问题。

1

以我愚蠢的观点,你以比实际更复杂的方式来思考它。为了恢复到之前的状态,根本不需要整个节点的克隆。而是每个* *命令类将有 -

  1. 参考它在作用于节点,
  2. 纪念对象(具有状态变量刚好够节点恢复到)
  3. 的execute()方法
  4. 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的书(在备忘录模式的讨论)链接的这一举动,在被某种约束求解器的处理节点的位置变化来读取。