4

我正在开发一个使用 Java 的小型 UML 编辑器项目,这是我几个月前开始的。几周后,我得到了一个 UML 类图编辑器的工作副本。

但是现在,我完全重新设计它以支持其他类型的图表,例如序列、状态、类等。这是通过实现一个图形构建框架来完成的(Cay Horstmann 在该主题上的工作给我很大启发紫罗兰色 UML 编辑器)。

重新设计进展顺利,直到我的一位朋友告诉我,我忘记在项目中添加 Do/Undo 功能,在我看来,这是至关重要的。

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

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

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

位置信息作为属性保存在节点中,因此通过修改节点本身中的属性,状态会改变。刷新显示时,节点将移动。这是模式的 Memento 部分(我认为),不同之处在于对象是状态本身。

此外,如果我保留原始节点的克隆(在它移动之前),我可以回到它的旧版本。相同的技术适用于节点中包含的信息(类或接口名称、注释节点的文本、属性名称等)。

问题是,在图中,如何在撤消/重做操作时将节点替换为其克隆?如果我克隆图表引用的原始对象(在节点列表中),则克隆不是图表中的引用,唯一指向的是命令本身!我是否应该在图中包含根据 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,我不必将图表传递给实际的节点或链接,以便它可以将自己从图表中移除(我认为这是一个坏主意)。

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 合并。这个类暂时是空的。我可能会使用一种机制来确定修改后的对象是节点还是边(通过 instanceof 或 ID 中的特殊表示)来执行 ID 操作。

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

4

2 回答 2

4

我认为您只需要将问题分解为较小的问题。

第一个问题: 问:如何用备忘录/命令模式来表示你的应用程序中的步骤?首先,我不知道你的应用程序是如何工作的,但希望你能看到我的目标。假设我想在图表上放置一个具有以下属性的 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 方法,除非您不能保证不会以导致哈希码相同的方式复制您的节点。

问题的下一部分是在您的 DiagramBuilder 中,因为您正试图弄清楚如何处理这些命令。为此,我只能说真的只是确保您可以为您可以添加的每种类型的组件创建一个反向操作。要处理断开连接,您可以查看边缘连接属性(我认为代码中的链接)并通知每个边缘连接它们将与特定节点断开连接。我会假设在断开连接时,他们可以适当地重新绘制自己。

总而言之,我建议不要在堆栈中保留对您的节点的引用,而只是一种表示当时给定节点状态的令牌。这将允许您在多个位置表示撤消堆栈中的同一节点,而无需引用同一对象。

如果你有 Q 就发帖。这是一个复杂的问题。

于 2008-10-17T04:47:40.600 回答
1

以我的拙见,您的想法比实际情况更复杂。为了恢复到以前的状态,根本不需要克隆整个节点。而是每个* *Command 类将具有 -

  1. 引用它所作用的节点,
  2. memento 对象(具有足以让节点恢复到的状态变量)
  3. 执行()方法
  4. 撤消()方法。

由于命令类具有对节点的引用,因此我们不需要 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 书(在纪念品模式讨论中)读到,随着节点位置变化的链接移动是由某种约束求解器处理的。

于 2011-05-02T18:26:43.780 回答