Pregunta

Estoy trabajando en un pequeño proyecto de editor UML, en Java, que comencé hace un par de meses. Después de unas semanas, obtuve una copia de trabajo para un editor de diagramas de clase UML.

Pero ahora, lo estoy rediseñando por completo para admitir otros tipos de diagramas, como secuencia, estado, clase, etc. Esto se realiza mediante la implementación de un marco de construcción de gráficos (estoy muy inspirado por el trabajo de Cay Horstmann en el sujeto con el editor Violet UML).

El rediseño se desarrolló sin problemas hasta que uno de mis amigos me dijo que olvidé agregar una funcionalidad de Hacer / Deshacer al proyecto, que, en mi opinión, es vital.

Recordando los cursos de diseño orientados a objetos, inmediatamente pensé en Memento y el patrón de Comando.

Aquí está el trato. Tengo una clase abstracta, AbstractDiagram, que contiene dos ArrayLists: una para almacenar nodos (llamados Elementos en mi proyecto) y la otra para almacenar Bordes (llamados Enlaces en mis proyectos). El diagrama probablemente mantendrá una pila de comandos que se pueden Deshacer / Rehacer. Bastante estándar.

¿Cómo puedo ejecutar estos comandos de una manera eficiente? Digamos, por ejemplo, que quiero mover un nodo (el nodo será un tipo de interfaz llamado INode, y habrá nodos concretos derivados de él (ClassNode, InterfaceNode, NoteNode, etc.)).

La información de posición se mantiene como un atributo en el nodo, por lo que al modificar ese atributo en el propio nodo, el estado cambia. Cuando la pantalla se actualice, el nodo se habrá movido. Esta es la parte Memento del patrón (creo), con la diferencia de que el objeto es el estado mismo.

Además, si mantengo un clon del nodo original (antes de que se moviera), puedo volver a su versión anterior. La misma técnica se aplica a la información contenida en el nodo (el nombre de clase o interfaz, el texto de un nodo de nota, el nombre de los atributos, etc.).

La cuestión es, ¿cómo puedo reemplazar, en el diagrama, el nodo con su clon tras la operación de deshacer / rehacer? Si clono el objeto original al que hace referencia el diagrama (que está en la lista de nodos), el clon no es referencia en el diagrama, ¡y lo único que apunta es el Comando mismo! ¿Debería incluir mecanismos en el diagrama para encontrar un nodo de acuerdo con una ID (por ejemplo) para poder reemplazar, en el diagrama, el nodo por su clon (y viceversa)? ¿Depende de los patrones de Memento y Comando hacer eso? ¿Qué hay de los enlaces? También deberían ser móviles, pero no quiero crear un comando solo para enlaces (y uno solo para nodos), y debería poder modificar la lista correcta (nodos o enlaces) de acuerdo con el tipo de objeto del comando se refiere a.

¿Cómo procederías? En resumen, tengo problemas para representar el estado de un objeto en un patrón de comando / recuerdo para poder recuperarlo de manera eficiente y restaurar el objeto original en la lista del diagrama, y ??dependiendo del tipo de objeto (nodo o enlace).

¡Muchas gracias!

Guillaume.

P.S .: si no estoy claro, dímelo y aclararé mi mensaje (¡como siempre!).

Editar

Aquí está mi solución real, que comencé a implementar antes de publicar esta pregunta.

Primero, tengo una clase AbstractCommand definida de la siguiente manera:

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();
}

Luego, cada tipo de comando se implementa utilizando una derivación concreta de AbstractCommand.

Entonces tengo un comando para mover un objeto:

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;
    }
}

También tengo un MoveRemoveCommand (para ... mover o eliminar un objeto / nodo). Si uso el ID del método instanceof, no tengo que pasar el diagrama al nodo o enlace real para que pueda eliminarse del diagrama (lo cual es una mala idea, creo).

Diagrama de diagrama abstracto;     Obj añadible;     Tipo 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;
        }
    }
}

Finalmente, tengo un ModificationCommand que se usa para modificar la información de un nodo o enlace (nombre de clase, etc.). Esto puede fusionarse en el futuro con MoveCommand. Esta clase está vacía por ahora. Probablemente haré lo de ID con un mecanismo para determinar si el objeto modificado es un nodo o un borde (a través de una instancia o una denotación especial en la ID).

¿Es esta una buena solución?

¿Fue útil?

Solución

Creo que solo necesita descomponer su problema en otros más pequeños.

Primer problema: P: ¿Cómo representar los pasos en su aplicación con el patrón de comando / recuerdo? En primer lugar, no tengo idea exactamente cómo funciona su aplicación, pero espero que vea a dónde voy con esto. Digamos que quiero colocar un ClassNode en el diagrama que con las siguientes propiedades

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

Eso sería envuelto como un objeto de comando. Digamos que va a un DiagramController. Luego, la responsabilidad del controlador del diagrama puede ser registrar ese comando (empujar en una pila sería mi apuesta) y pasar el comando a un DiagramBuilder, por ejemplo. DiagramBuilder sería el responsable de actualizar la pantalla.

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);
  }
}

Algo así debería hacerlo y, por supuesto, habrá muchos detalles para resolver. Por cierto, cuantas más propiedades tengan sus nodos, más detallado tendrá que ser Undraw.

Usar una identificación para vincular el comando en su pila al elemento dibujado podría ser una buena idea. Eso podría verse así:

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);
  }
} 

En este punto, no es absolutamente necesario que tenga el objeto ya que tiene la ID, pero será útil si decide también implementar la función de rehacer. Una buena forma de generar la identificación para sus nodos sería implementar un método de código hash para ellos, excepto por el hecho de que no se garantizaría que no duplique sus nodos de tal manera que el código hash sea idéntico.

La siguiente parte del problema está dentro de su DiagramBuilder porque está tratando de averiguar cómo diablos lidiar con estos comandos. Para eso, todo lo que puedo decir es que realmente solo asegure que pueda crear una acción inversa para cada tipo de componente que pueda agregar. Para manejar la desvinculación, puede mirar la propiedad de conexión de borde (enlaces en su código, creo) y notificar a cada una de las conexiones de borde que deben desconectarse del nodo específico. Supongo que en caso de desconexión, podrían redibujarse adecuadamente.

Para resumir un poco, recomendaría no mantener una referencia a sus nodos en la pila, sino un tipo de token que represente el estado de un nodo determinado en ese punto. Esto le permitirá representar el mismo nodo en su pila de deshacer en varios lugares sin que se refiera al mismo objeto.

Publica si tienes Q's. Este es un problema complejo.

Otros consejos

En mi humilde opinión, lo estás pensando de una manera más complicada de lo que realmente es. Para volver al estado anterior, no es necesario clonar el nodo completo. Más bien cada clase de comando * * tendrá -

  1. referencia al nodo sobre el que está actuando,
  2. objeto de recuerdo (que tiene las variables de estado lo suficiente para que el nodo vuelva)
  3. método de ejecución ()
  4. método de deshacer ().

Dado que las clases de comando tienen referencia al nodo, no necesitamos un mecanismo de ID para hacer referencia a los objetos en el diagrama.

En el ejemplo de su pregunta, queremos mover un nodo a una nueva posición. Para eso, tenemos una clase 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
    }
}
  
    

¿Qué pasa con los enlaces? También deberían ser móviles, pero no quiero crear un comando solo para enlaces (y uno solo para nodos).

  

Leí en el libro GoF (en la discusión del patrón de recuerdo) que el movimiento del enlace con el cambio en la posición de los nodos es manejado por algún tipo de solucionador de restricciones.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top