【问题标题】:Storing the state of a complex object with Memento pattern (and Command)使用备忘录模式(和命令)存储复杂对象的状态
【发布时间】:2010-09-17 16:24:41
【问题描述】:

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

但现在,我正在重新设计它以支持其他类型的图表,例如序列、状态、类等。这是通过实现图形构建框架来完成的(Cay Horstmann 在使用 Violet UML 编辑器的主题)。

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

想起面向对象的设计课程,我立刻想到了备忘录和命令模式。

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

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

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

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

问题是,在图中,如何在撤消/重做操作时将节点替换为其克隆?如果我克隆图表引用的原始对象(在节点列表中),则克隆不是图表中的引用,唯一指向的是命令本身!我是否应该在图中包含根据 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 合并。这个类暂时是空的。我可能会使用一种机制来确定被修改的对象是节点还是边(通过 instanceof 或 ID 中的特殊表示)。

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

【问题讨论】:

    标签: java design-patterns command state memento


    【解决方案1】:

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

    第一个问题: 问:如何用备忘录/命令模式表示您的应用程序中的步骤? 首先,我不知道你的应用程序是如何工作的,但希望你能看到我的目标。假设我想在图表上放置一个具有以下属性的 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,请发布。这是一个复杂的问题。

    【讨论】:

      【解决方案2】:

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

      1. 对其作用的节点的引用,
      2. memento 对象(具有足以让节点恢复到的状态变量)
      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 书(在备忘录模式讨论中)读到,链接的移动与节点位置的变化是由某种约束求解器处理的。

      【讨论】:

        猜你喜欢
        • 2013-12-01
        • 2013-08-27
        • 2011-03-16
        • 1970-01-01
        • 2018-08-12
        • 2012-07-09
        • 2011-03-26
        • 2013-12-20
        • 1970-01-01
        相关资源
        最近更新 更多