java - 使用备忘录模式(和命令)存储复杂对象的状态

标签 java design-patterns command state memento

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

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

重新设计进行得很顺利,直到我的一位 friend 告诉我,我忘记在项目中添加 Do/Undo 功能,在我看来,这很重要。

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

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

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

位置信息作为节点中的一个属性保存,因此通过在节点本身中修改该属性,状态会发生变化。刷新显示时,节点将已移动。这是模式的纪念品部分(我认为),区别在于对象是状态本身。

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

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

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

最佳答案

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

第一个问题:
问:如何用备忘录/命令模式表示您的应用程序中的步骤?
首先,我不知道您的应用程序究竟是如何工作的,但希望您能明白我的意图。假设我想在图表上放置一个具有以下属性的 ClassNode

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

这将被包装为一个命令对象。假设去往一个 DiagramController。然后,图表 Controller 的职责可以是记录该命令(我敢打赌将其插入堆栈)并将该命令传递给例如 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 就发帖吧。这是一个复杂的问题。

关于java - 使用备忘录模式(和命令)存储复杂对象的状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/211034/

相关文章:

java - 将 clojure 作为 JAVA 文件运行

java - Eclipse 不显示输出

design-patterns - 到底是不是所有的设计都很难维护呢?

python - 如何以模块化的方式设计应用程序?

linux - 如何在linux命令中替换多个文件中的字符串

java - 如何在Java中执行sudo命令并获得错误输出?

java - 如何编写一个通用的数字相加方法

java - 多锁 - 幕后花絮

design-patterns - 何时使用桥接模式以及它与适配器模式有何不同?

Unix cp 命令目标 = . (点)?